@asmlift/core 0.5.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// L3 pass: hoist a
|
|
2
|
-
// typed local pointer.
|
|
1
|
+
// L3 pass: hoist a leaf pointer base (a global address or a numeric pointer constant) into a
|
|
2
|
+
// typed local pointer. HOW MANY uses a base needs is the gate table's question, not the pass's:
|
|
3
|
+
// the default table wants 2+, `BASEFOLD_GATES` admits one under the conditions below.
|
|
3
4
|
//
|
|
4
5
|
// A base indexed at 2+ sites — `((u8 *)&gTable)[i+5]` and `[i+6]`, or the MMIO/RAM constant
|
|
5
6
|
// `((s32 *)0x40000d4)[0]`, `[1]`, `[2]` — re-materialized the address (a fresh pool load) at each
|
|
@@ -8,21 +9,108 @@
|
|
|
8
9
|
// base into a local pointer `T *p = (T *)base` and points each access at `p`, so the recompiled code
|
|
9
10
|
// keeps the address in one register instead of reloading it.
|
|
10
11
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// WHICH bases. The pass hoists every key its gate list admits, which answers per FUNCTION a
|
|
13
|
+
// question the source answered per BASE — one register file spelled as a pointer local beside
|
|
14
|
+
// scalar cells spelled as bare derefs. The `single-cell` gate is what makes the narrower answer
|
|
15
|
+
// reachable: under `LIVEBASE_BLOCK_GATES` a base every access of which is ONE fixed offset stays
|
|
16
|
+
// inline, and rank's LIVEBASE_HOISTS roster emits each table's hoist — and every composition onto
|
|
17
|
+
// it — as its own candidate family, for the differ to referee between them. The unit is
|
|
18
|
+
// the (base, width, signedness) KEY, not the base — a base read at two widths is two keys, and the
|
|
19
|
+
// gate can leave one of them inline while the other binds.
|
|
20
|
+
//
|
|
21
|
+
// COVERAGE: the roster (rank.ts) is SEVEN rows over FIVE gate tables — two PAIRS share a table and
|
|
22
|
+
// differ only in placement, `/basefold` with `/basefold/sinkinit` and `/orderbase` with
|
|
23
|
+
// `/orderbase-scoped` — and it is a set of hand-picked SUBSETS rather than a narrowness ranking;
|
|
24
|
+
// only `/livebase` ⊇ `/livebase-block` are ordered by inclusion. A table
|
|
25
|
+
// whose predicate cuts across the others therefore carves out a PARTIAL answer, which is what
|
|
26
|
+
// `UNFOLDED_GATES` does. Measured at ONE stated scope, `decompile()`'s default structuring,
|
|
27
|
+
// map-less, one tree per row over the 363 agbcc rows the artifact carried when the census ran
|
|
28
|
+
// (23 unstructurable):
|
|
29
|
+
// `admittedBases(sfn, UNFOLDED_GATES)` is a PROPER NONEMPTY SUBSET of
|
|
30
|
+
// `admittedBases(sfn, LIVEBASE_GATES)` on THIRTEEN of them — `kleod:ConfigureEntityBehavior` 3→2,
|
|
31
|
+
// `kleod:ProcessInputAndUpdateEntities` 7→5, `kleod:UpdateCameraScroll` 5→2,
|
|
32
|
+
// `sa3:Sio32MultiLoadIntr` 5→1 and `synthetic:unfoldpark` 3→2 among them — and on
|
|
33
|
+
// `synthetic:dmascope`, a function with THREE bases `single-cell` does not reject, exactly ONE
|
|
34
|
+
// binds. So "some of the several" is reachable wherever a PREDICATE separates the bases. What
|
|
35
|
+
// stays unreachable is an ARBITRARY subset: a table is a conjunction of gates and so denotes ONE
|
|
36
|
+
// predicate over `BaseKey`, which means the only subsets on offer are the ones some predicate
|
|
37
|
+
// carves, and choosing per key by score is the per-base policy assignment nobody has built.
|
|
38
|
+
// EVERY COUNT HERE IS SCOPED TO THAT ONE STRUCTURING and to the artifact's row count on the day —
|
|
39
|
+
// a wider census (every structuring `enumerateCandidates` builds) sees keys this one never mints,
|
|
40
|
+
// and the corpus grows. Re-run rather than read.
|
|
41
|
+
//
|
|
42
|
+
// WHAT THE ASM SUGGESTS, AND WHY IT IS A CANDIDATE. `single-use` refuses a base reached once, on
|
|
43
|
+
// the theory that one access re-materializes as cheaply as a named local. A surviving `[rN, #imm]`
|
|
44
|
+
// off a register holding a bare address is EVIDENCE against that theory, on a compiler that folds
|
|
45
|
+
// a constant SUBSCRIPT into the literal it materializes
|
|
46
|
+
// (TargetDescription.compilerBehaviors.foldsConstAddrOffset, where the compiled pair is).
|
|
47
|
+
// `BaseKey.unfoldedOffset` is that shape: the access's constant offset reached the MEMORY OPERAND
|
|
48
|
+
// rather than the materialized literal, which `l3/ast.ts`'s `index.operandOff` carries down from
|
|
49
|
+
// the lift because the fold at L3 makes the two indistinguishable.
|
|
50
|
+
// HOW STRONG THE EVIDENCE IS, compiled in both directions rather than reasoned about — and the
|
|
51
|
+
// answer differs between the two base kinds, so read the one you are looking at.
|
|
52
|
+
// For a NUMERIC base every inline shape tried spends the offset somewhere the operand does not
|
|
53
|
+
// see it: one read folds it into the literal (`.word 0x3001103` + `ldrb [r0]`); several reads at
|
|
54
|
+
// several offsets share ONE pool word and pay `sub`/`add` per access; a store pair agbcc CSEs
|
|
55
|
+
// (`*(u8 *)0x3001100 = v; *(u16 *)0x3001102 = v;` → one pool word plus `add r1, r1, #0x2`) spends
|
|
56
|
+
// it on an add, which the frontend folds back into the address; and a read whose bare address is
|
|
57
|
+
// ALSO used as a value takes a SECOND literal (`.word 0x3001100` + `.word 0x3001103`) rather than
|
|
58
|
+
// an operand offset.
|
|
59
|
+
// For a SYMBOL base that last shape is a COUNTEREXAMPLE, and it is the one thing that separates
|
|
60
|
+
// the two. `void live(void){ sink((int)(u8 *)&gS); sink(((u8 *)&gS)[3]); }` emits ONE `.word gS`
|
|
61
|
+
// and `ldrb r0, [r4, #0x3]` — agbcc CSEs the symbol reference where it re-materializes the integer
|
|
62
|
+
// — so the inline subscript spelling produces exactly the shape this rule reads as evidence
|
|
63
|
+
// against it. asmlift lifts that asm back to the correct `((u8 *)&gS)[3]` and then offers the
|
|
64
|
+
// spelling through a named base anyway. Measured reach: of the 21 keys the symbol half newly admits over
|
|
65
|
+
// the artifact's agbcc rows in both symbol-map configurations, 4 are on a base whose address the
|
|
66
|
+
// tree also uses as a value (2 distinct keys, on `kleod:ProcessInputAndUpdateEntities` and
|
|
67
|
+
// `pokeemerald:TrySetCantSelectMoveBattleScript`).
|
|
68
|
+
// So on the symbol half this is weaker than evidence-with-two-known-exceptions: it is a hint with
|
|
69
|
+
// a live counterexample, which is precisely why it is a ROSTER ADMISSION and not a gate relaxation
|
|
70
|
+
// — the inline spelling rides beside it in every case and `compareScored` orders by score, so the
|
|
71
|
+
// hint FIRING wrongly costs a candidate compile and never a match. Note which direction that
|
|
72
|
+
// covers: it does not say the flag is free to LOSE. A `/basefold*` candidate that is never
|
|
73
|
+
// enumerated takes whatever it would have won with it — deleting the HEAD roster row costs
|
|
74
|
+
// `synthetic:foldhead` its match — which is why `index.operandOff` is carried from the lift
|
|
75
|
+
// instead of re-derived, and why a committed pass that can drop it is worth a test
|
|
76
|
+
// (test/basecse.test.ts, the "`operandOff` is provenance" describe). WHAT EACH ROW IS WORTH is
|
|
77
|
+
// measured in rank.ts's note on `BASEFOLD_HOISTS`, not here, and the two rows are not worth
|
|
78
|
+
// the same thing. Promoting the hint to a default would need this paragraph to say something it
|
|
79
|
+
// does not.
|
|
80
|
+
//
|
|
81
|
+
// It is EVIDENCE and not proof, which is why `BASEFOLD_GATES` below backs a variation rather than a
|
|
82
|
+
// relaxation of the default table. agbcc folds a subscript but keeps an aggregate MEMBER offset in
|
|
83
|
+
// the memory operand: `((struct S *)0x3001100)->b` emits `.word 0x3001100` + `ldr [r0, #0x4]`,
|
|
84
|
+
// byte-identical to the spelling through a named base, and the same holds for a union member and for a
|
|
85
|
+
// store. So the shape has two sources and asmlift can spell only one of them; rank.ts offers both
|
|
86
|
+
// and the differ referees.
|
|
87
|
+
//
|
|
88
|
+
// A SYMBOL base reads the same rule, one exception weaker (above). agbcc folds a symbol's offset
|
|
89
|
+
// as it folds a numeric one — `((u8 *)&gSym)[3]` emits `.word gSym+0x3` + `ldrb [r1]` where
|
|
90
|
+
// `gSym.d` and `u8 *p = (u8 *)&gSym; p[3]` both emit `.word gSym` + `ldrb [r1, #0x3]` — and the
|
|
91
|
+
// relocation's addend arrives as an explicit `add` where the operand offset arrives as the load's
|
|
92
|
+
// own, so the two are one flag apart at the point the offsets fold together.
|
|
93
|
+
//
|
|
94
|
+
// SCOPE / SOUNDNESS. Only an `index` node whose base is a bare `addr` (a global address), a bare
|
|
95
|
+
// `const` (a numeric pointer address) or a REINTERPRET CAST of one of those (see `isHoistableBase`)
|
|
96
|
+
// is eligible, keyed by (base, width, signedness) — never an
|
|
97
|
+
// AGGREGATE base (F9 spells a SCALAR global as a bare `var`, which is never an `index`-of-leaf, so
|
|
98
|
+
// scalar recovery is untouched). Non-leaf bases (a local, a struct-element `p[a0]`,
|
|
99
|
+
// arithmetic) are excluded: agbcc may re-derive those, so hoisting them can
|
|
100
|
+
// MISMATCH (empirically confirmed) — the differ-refereed `/addr-home` variation
|
|
101
|
+
// (structure/analysis.ts homeSharedAddresses) serves the shared gaddr-free ARITHMETIC bases
|
|
102
|
+
// instead.
|
|
103
|
+
// The hoisted local carries the access's pointer type, so the
|
|
17
104
|
// deref cast the C backend applied inline at each `index` now lands ONCE on the local's initializer
|
|
18
105
|
// and the accesses stride correctly with no per-use cast. A wrong hoist (a base agbcc would actually
|
|
19
106
|
// re-materialize) only changes recompiled bytes -> a LOST match under the zero-lost gate, never a
|
|
20
107
|
// miscompile: the address value is identical, just held in a different place.
|
|
21
|
-
import {
|
|
108
|
+
import { assertHoistsDominate } from '../contracts';
|
|
109
|
+
import { type IrType, T, scalarTypeForAccess, typeToString } from '../ir/types';
|
|
22
110
|
import type { Expr, SFn, Stmt } from './ast';
|
|
23
|
-
import { mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
24
|
-
import { type Gate, firstRejection } from './gates';
|
|
25
|
-
import { nameAllocator } from './hoist';
|
|
111
|
+
import { exprChildren, mapExprChildren, mapStmtExprs, stmtChildren, stmtExprs } from './ast';
|
|
112
|
+
import { type Gate, ablateHeuristic, firstRejection } from './gates';
|
|
113
|
+
import { type BaseInit, type HoistPlacement, nameAllocator, placeBaseLocals } from './hoist';
|
|
26
114
|
|
|
27
115
|
// A HOISTABLE base is a bare `addr` (a global address) or a bare `const` (a numeric pointer
|
|
28
116
|
// address). Both are relocation-invariant leaves whose value the compiler keeps in one register
|
|
@@ -31,18 +119,71 @@ import { nameAllocator } from './hoist';
|
|
|
31
119
|
// argbase.ts take is the obvious consolidation and it is wrong twice over: this pass has no `lead`
|
|
32
120
|
// handling, so a rank-aware `g[0][i]` comes out as `p[0][i]` through a scalar pointer, and it
|
|
33
121
|
// undoes raise/gvn.ts's hoist on exactly the rows a symbol map serves (test/addr-placement.test.ts).
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
122
|
+
//
|
|
123
|
+
// …and a REINTERPRET CAST of one of those leaves, `((struct S *)&gSym)[i]`, which is the same leaf
|
|
124
|
+
// wearing the pointer type its element needs. The structurer emits it for an array of STRUCTS
|
|
125
|
+
// (structure.ts arrayAccess's `fieldOff` path), where the scalar `scalarTypeForAccess` this pass
|
|
126
|
+
// otherwise mints is meaningless — a 28-byte element has no `intType`. The cast is the base's
|
|
127
|
+
// SPELLING rather than a different base, so it hoists to `struct S *p = (struct S *)&gSym`, the
|
|
128
|
+
// accesses stride it exactly as a scalar key's do, and the KEY carries the cast's target type
|
|
129
|
+
// because two casts over one symbol stride differently and are two locals. Refused when the cast's
|
|
130
|
+
// target is not a pointer (nothing an index can stride) and when it is `volatile`: dropping that
|
|
131
|
+
// qualifier onto a non-volatile local makes every access through the local a plain one, a silent
|
|
132
|
+
// change of meaning rather than of bytes — volatility on a hoisted local is `l3/volatileptr.ts`'s
|
|
133
|
+
// question, asked of the local and not of the leaf.
|
|
134
|
+
type HoistableLeaf = Extract<Expr, { k: 'addr' } | { k: 'const' }>;
|
|
135
|
+
type HoistableCast = Extract<Expr, { k: 'cast' }> & { e: HoistableLeaf };
|
|
136
|
+
type HoistableBase = HoistableLeaf | HoistableCast;
|
|
137
|
+
const isHoistableLeaf = (e: Expr): e is HoistableLeaf => e.k === 'addr' || e.k === 'const';
|
|
138
|
+
const isHoistableBase = (e: Expr): e is HoistableBase =>
|
|
139
|
+
isHoistableLeaf(e) || (e.k === 'cast' && e.to.kind === 'ptr' && e.volatile !== true && isHoistableLeaf(e.e));
|
|
140
|
+
/** A hoistable LEAF's identity, ignoring the access width: `a:<symbol>` or `c:<numeric address>`.
|
|
141
|
+
* Exported because `l3/homesplit.ts` identifies bases in this same vocabulary, and two producers
|
|
142
|
+
* of one string are two places for the spelling to drift. */
|
|
143
|
+
export const leafId = (b: HoistableLeaf): string => (b.k === 'addr' ? `a:${b.name}` : `c:${b.value}`);
|
|
144
|
+
const baseId = (b: HoistableBase): string => (b.k === 'cast' ? `${leafId(b.e)} <${typeToString(b.to)}>` : leafId(b));
|
|
37
145
|
|
|
38
146
|
/** The (base, access-shape) key an `index`-of-hoistable-base shares with its reuse siblings. */
|
|
39
147
|
const keyOf = (base: HoistableBase, width: number, signed: boolean): string => `${baseId(base)} ${width} ${signed}`;
|
|
40
148
|
|
|
149
|
+
/** The key's own grammar, read back — `<leafId>[ <type>] <width> <signed>`.
|
|
150
|
+
*
|
|
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'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
|
+
* for the next base kind (`homeSplitTag` states the one the cast form causes).
|
|
155
|
+
*
|
|
156
|
+
* The one space inside a cast's base id is this grammar's own separator, not the type's: every
|
|
157
|
+
* type this pass can put there spells without one (`u16*`, `Struct0`, `u8[4]`). A struct's name is
|
|
158
|
+
* DATA, though — synthetic today, DWARF later — so the parse below reads the type as everything
|
|
159
|
+
* between the separator and the closing `>` rather than as one word. */
|
|
160
|
+
export interface BaseKeyParts {
|
|
161
|
+
/** the hoistable leaf, `a:<symbol>` or `c:<numeric address>` */
|
|
162
|
+
readonly leaf: string;
|
|
163
|
+
/** the reinterpret cast's target type as `typeToString` spells it, or null for a bare leaf */
|
|
164
|
+
readonly castType: string | null;
|
|
165
|
+
readonly width: number;
|
|
166
|
+
readonly signed: boolean;
|
|
167
|
+
}
|
|
168
|
+
export function parseBaseKey(key: string): BaseKeyParts {
|
|
169
|
+
const parts = key.split(' ');
|
|
170
|
+
const signed = parts.pop() === 'true';
|
|
171
|
+
const width = Number(parts.pop());
|
|
172
|
+
const id = parts.join(' ');
|
|
173
|
+
const lt = id.indexOf(' <');
|
|
174
|
+
return {
|
|
175
|
+
leaf: lt === -1 ? id : id.slice(0, lt),
|
|
176
|
+
castType: lt === -1 ? null : id.slice(lt + 2, -1),
|
|
177
|
+
width,
|
|
178
|
+
signed,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
41
182
|
interface Collected {
|
|
42
183
|
count: Map<string, number>;
|
|
43
184
|
order: string[];
|
|
44
185
|
meta: Map<string, { base: HoistableBase; width: number; signed: boolean }>;
|
|
45
|
-
/** keys with ANY use inside a loop — disqualified (see the loop note in `
|
|
186
|
+
/** keys with ANY use inside a loop — disqualified (see the loop note in `hoistBaseLocals`). */
|
|
46
187
|
inLoop: Set<string>;
|
|
47
188
|
/** per key, how many times each CONSTANT offset was accessed — the input to the
|
|
48
189
|
* `repeated-const-offset` gate, which losing the ProcessHBlankWait match is what bought. A
|
|
@@ -50,9 +191,21 @@ interface Collected {
|
|
|
50
191
|
* tallied); a repeat means a scalar re-access, and ONE is enough to disqualify the base even
|
|
51
192
|
* when it also has distinct-offset uses. */
|
|
52
193
|
constOffCount: Map<string, Map<number, number>>;
|
|
194
|
+
/** keys indexed by a NON-constant expression somewhere — an array walk, so the base reaches a
|
|
195
|
+
* BLOCK of cells however few constant offsets it also touches (see `single-cell`). */
|
|
196
|
+
varIndexed: Set<string>;
|
|
197
|
+
/** keys with an access whose constant offset arrived in the MEMORY OPERAND (l3/ast.ts
|
|
198
|
+
* `index.operandOff`) — the input to `unfoldedOffset`. */
|
|
199
|
+
operandOff: Set<string>;
|
|
200
|
+
/** keys EVERY access of which carries the order licence (l3/ast.ts `index.baseOrdered`) — the
|
|
201
|
+
* input to `orderLicensed`. Per key rather than per access because a home is one decision for
|
|
202
|
+
* the whole key; the licence is derived per SYMBOL upstream, so the two only disagree where a
|
|
203
|
+
* pass has rebuilt one node and dropped the stamp, and requiring all of them makes that a
|
|
204
|
+
* refusal rather than a half-homed base. */
|
|
205
|
+
ordered: Map<string, boolean>;
|
|
53
206
|
}
|
|
54
207
|
|
|
55
|
-
/** Every `index` node whose base is a hoistable leaf, tallied by key (
|
|
208
|
+
/** Every `index` node whose base is a hoistable leaf, tallied by key (the gates' use count) and in
|
|
56
209
|
* first-appearance order (so the hoisted assignments emit in the order the bases are first used,
|
|
57
210
|
* matching the compiler's pool-load order). `loop` marks uses nested in a while/do-while/for. */
|
|
58
211
|
function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
@@ -70,9 +223,17 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
70
223
|
if (e.idx.k === 'const') {
|
|
71
224
|
const m = c.constOffCount.get(k) ?? c.constOffCount.set(k, new Map()).get(k)!;
|
|
72
225
|
m.set(e.idx.value, (m.get(e.idx.value) ?? 0) + 1);
|
|
226
|
+
} else {
|
|
227
|
+
c.varIndexed.add(k);
|
|
228
|
+
}
|
|
229
|
+
// `!== undefined`, never truthiness: the field carries a displacement, and a negative one
|
|
230
|
+
// is a real access (`lw v0, -8(a1)`).
|
|
231
|
+
if (e.operandOff !== undefined) {
|
|
232
|
+
c.operandOff.add(k);
|
|
73
233
|
}
|
|
234
|
+
c.ordered.set(k, (c.ordered.get(k) ?? true) && e.baseOrdered === true);
|
|
74
235
|
}
|
|
75
|
-
for (const ch of
|
|
236
|
+
for (const ch of exprChildren(e)) {
|
|
76
237
|
visitExpr(ch, inLoop);
|
|
77
238
|
}
|
|
78
239
|
};
|
|
@@ -87,16 +248,6 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
87
248
|
}
|
|
88
249
|
}
|
|
89
250
|
|
|
90
|
-
// local re-export to avoid importing exprChildren twice (mapExprChildren covers rewrite).
|
|
91
|
-
function exprChildrenOf(e: Expr): Expr[] {
|
|
92
|
-
const out: Expr[] = [];
|
|
93
|
-
mapExprChildren(e, (c) => {
|
|
94
|
-
out.push(c);
|
|
95
|
-
return c;
|
|
96
|
-
});
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
251
|
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local. */
|
|
101
252
|
function rewrite(e: Expr, localFor: Map<string, string>): Expr {
|
|
102
253
|
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
@@ -108,37 +259,6 @@ function rewrite(e: Expr, localFor: Map<string, string>): Expr {
|
|
|
108
259
|
return mapExprChildren(e, (c) => rewrite(c, localFor));
|
|
109
260
|
}
|
|
110
261
|
|
|
111
|
-
function rewriteStmt(s: Stmt, localFor: Map<string, string>): Stmt {
|
|
112
|
-
const mapS = (x: Stmt): Stmt => rewriteStmt(x, localFor);
|
|
113
|
-
switch (s.k) {
|
|
114
|
-
case 'assign':
|
|
115
|
-
return { ...s, value: rewrite(s.value, localFor) };
|
|
116
|
-
case 'store':
|
|
117
|
-
return { ...s, lval: rewrite(s.lval, localFor), value: rewrite(s.value, localFor) };
|
|
118
|
-
case 'exprstmt':
|
|
119
|
-
return { ...s, value: rewrite(s.value, localFor) };
|
|
120
|
-
case 'return':
|
|
121
|
-
return s.value ? { ...s, value: rewrite(s.value, localFor) } : s;
|
|
122
|
-
case 'if':
|
|
123
|
-
return { ...s, cond: rewrite(s.cond, localFor), then: s.then.map(mapS), else: s.else.map(mapS) };
|
|
124
|
-
case 'while':
|
|
125
|
-
case 'dowhile':
|
|
126
|
-
return { ...s, cond: rewrite(s.cond, localFor), body: s.body.map(mapS) };
|
|
127
|
-
case 'for':
|
|
128
|
-
return { ...s, init: mapS(s.init), cond: rewrite(s.cond, localFor), inc: mapS(s.inc), body: s.body.map(mapS) };
|
|
129
|
-
case 'switch':
|
|
130
|
-
return {
|
|
131
|
-
...s,
|
|
132
|
-
scrutinee: rewrite(s.scrutinee, localFor),
|
|
133
|
-
cases: s.cases.map((c) => ({ ...c, body: c.body.map(mapS) })),
|
|
134
|
-
default: s.default?.map(mapS),
|
|
135
|
-
};
|
|
136
|
-
case 'break':
|
|
137
|
-
case 'continue':
|
|
138
|
-
return s;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
262
|
/** One base under consideration, keyed as `(base, width, signedness)`. */
|
|
143
263
|
export interface BaseKey {
|
|
144
264
|
key: string;
|
|
@@ -146,6 +266,100 @@ export interface BaseKey {
|
|
|
146
266
|
inLoop: boolean;
|
|
147
267
|
/** some CONSTANT offset through this base is touched 2+ times */
|
|
148
268
|
repeatedConstOffset: boolean;
|
|
269
|
+
/** every access is the SAME fixed offset — one scalar cell rather than a block of them */
|
|
270
|
+
singleCell: boolean;
|
|
271
|
+
/** A base whose constant offset arrived in the MEMORY OPERAND (l3/ast.ts `index.operandOff`).
|
|
272
|
+
* On a compiler that folds a constant subscript into the literal it materializes, an offset
|
|
273
|
+
* that reached the instruction instead survived because something OTHER than a subscript put it
|
|
274
|
+
* there — a named base, or an aggregate member (see the header). TWO gate tables read it, both
|
|
275
|
+
* on roster rows rank.ts offers only where the target declares the fold, and they ask OPPOSITE
|
|
276
|
+
* questions: `BASEFOLD_GATES` EXEMPTS `single-use` on it, `UNFOLDED_GATES` REQUIRES it. Neither
|
|
277
|
+
* subtracts the other, so ablating one prices one.
|
|
278
|
+
*
|
|
279
|
+
* Where the fold leaves no evidence to read is decided UPSTREAM and once, in
|
|
280
|
+
* `structure/structure.ts` (`const fromOperand = off !== 0 ? { operandOff: off } : {}`). THREE
|
|
281
|
+
* ways to get an absence:
|
|
282
|
+
* • an offset the address expression carried (a relocation addend, a folded `add`);
|
|
283
|
+
* • an offset of 0;
|
|
284
|
+
* • an offset the INSTRUCTION COULD NOT ENCODE — not upstream's doing, and the one to watch,
|
|
285
|
+
* because the flag then reads the same for BOTH spellings and the gate is blind rather than
|
|
286
|
+
* unpersuaded. Thumb's load displacement is a scaled imm5 — 31 bytes for `ldrb`, 62 for
|
|
287
|
+
* `ldrh`, 124 for `ldr` — and past it agbcc materializes the pointer local with an explicit
|
|
288
|
+
* `add`: `u16 *p = (u16 *)&gBgInfo; p[36]` compiles to `.word gBgInfo` +
|
|
289
|
+
* `add r4, r4, #0x48` + `ldrh r0, [r4]` (no operand offset), against the inline
|
|
290
|
+
* `((u16 *)&gBgInfo)[36]`'s `.word gBgInfo+0x48` + `ldrh r0, [r4]` (also none). At an
|
|
291
|
+
* in-range subscript the same pair is `.word gBgInfo` + `ldrh [r4, #0xa]` against
|
|
292
|
+
* `.word gBgInfo+0xa` + `ldrh [r4]`, so the field discriminates there and only there.
|
|
293
|
+
*
|
|
294
|
+
* A GROUP OF TWO OR MORE ACCESSES PAST THE RANGE INVERTS THE SENSE, which is worse than
|
|
295
|
+
* blind: the fold SHIFTS the literal and the displacements resume relative to the shifted
|
|
296
|
+
* base, so it is the INLINE spelling that sets the flag. It bites only where a table is
|
|
297
|
+
* CONSULTED — a key `BASECSE_GATES` rejects, i.e. one reached inside a loop, since the
|
|
298
|
+
* default hoist binds anything else first; on a straight-line pair `admittedBases` is empty
|
|
299
|
+
* for every table here and for an EMPTY gate list, so that shape says nothing either way.
|
|
300
|
+
* In a loop over `((u16 *)&gBgInfo)[36]` and `[37]` the inline spelling emits
|
|
301
|
+
* `.word gBgInfo+0x48` + `ldrh [r5]` + `ldrh [r5, #0x2]` while the pointer-local twin emits
|
|
302
|
+
* `.word gBgInfo` + two `add`s and no operand offset at all; lifted, `UNFOLDED_GATES`
|
|
303
|
+
* admits `a:gBgInfo 2 false` on the INLINE one (2 of its 8 candidates carry `/unfolded`)
|
|
304
|
+
* and NOTHING on the local one. So on such a group `folded-offset` admits the key the
|
|
305
|
+
* reference spelled inline and refuses the key it parked — backwards, costing fan and a
|
|
306
|
+
* tie-break and never meaning, since the inline spelling is enumerated beside it. The two
|
|
307
|
+
* spellings still differ in the asm — a bare `.word` plus an `add` against a `.word` with
|
|
308
|
+
* the addend baked in — but not through THIS field, and no field carries it.
|
|
309
|
+
*
|
|
310
|
+
* HOW BIG THE INVERTED CLASS IS, at ONE stated scope — `decompile()`'s default structuring,
|
|
311
|
+
* map-less, one tree per row over the 363 agbcc rows the artifact carried when the census
|
|
312
|
+
* ran (23 unstructurable):
|
|
313
|
+
* `folded-offset` ADMITS 43 keys and ZERO of them have every nonzero displacement past the
|
|
314
|
+
* range for their width, so the inverted class has no corpus inhabitant at this scope; it
|
|
315
|
+
* REJECTS 40, of which 36 have no nonzero constant displacement at all (the two documented
|
|
316
|
+
* causes) and FOUR are past the range — `ConfigureEntityBehavior` gBgInfo 72,
|
|
317
|
+
* `EntityItemDrop` gEntity 504/506, `ProcessInputAndUpdateEntities` gUnk_03005220 76,
|
|
318
|
+
* `TransformSingleEntityToScreen` gUnk_03003430 64/66. A WIDER census — every structuring
|
|
319
|
+
* `enumerateCandidates` builds — mints keys this one never sees and reads higher on both
|
|
320
|
+
* sides, so quote the scope with the number and re-run rather than compare across scopes.
|
|
321
|
+
* Reaching the rejected four needs a NEW piece of evidence recorded upstream, not a relaxed
|
|
322
|
+
* gate here: widening `folded-offset` to fire on absence would make it fire exactly where
|
|
323
|
+
* its premise is false.
|
|
324
|
+
* Nothing is subtracted again here. A base of 0 is NOT a second refusal, tempting as it
|
|
325
|
+
* looks: on MIPS `((s8 *)0)[16]` really is one `lb $v0, 16($zero)` with nowhere else for the
|
|
326
|
+
* offset to be, but this rule runs only where `foldsConstAddrOffset` is declared, and agbcc
|
|
327
|
+
* materializes a zero base like any other — `mov r0, #0x10` + `ldrb [r0, #0]` inline against
|
|
328
|
+
* `s8 *p = (s8 *)0; p[16]`'s `mov r0, #0x0` + `ldrb [r0, #0x10]`, the same discriminating pair
|
|
329
|
+
* as at 0x3001100. Refusing it costs nothing here either way (no agbcc tree in the corpus
|
|
330
|
+
* reaches a base-0 access with an operand offset, in either symbol-map configuration), which is
|
|
331
|
+
* exactly why an unmeasured clause could sit in it. */
|
|
332
|
+
unfoldedOffset: boolean;
|
|
333
|
+
/** The base is a REINTERPRET CAST of a leaf (`(struct S *)&gSym`) rather than the leaf itself —
|
|
334
|
+
* the array-of-struct element shape. Its own field because it is about what the base IS, not
|
|
335
|
+
* about how often it is reached, and because every shipped table refuses it EXCEPT
|
|
336
|
+
* `ORDERBASE_GATES`, which ablates `cast-base`: the default spelling of a struct element is the
|
|
337
|
+
* inline cast, and homing it is a candidate `/orderbase` offers where the assembly licenses it. */
|
|
338
|
+
castBase: boolean;
|
|
339
|
+
/** No access through this base scaled the index before the base was materialized, and at least
|
|
340
|
+
* one materialized the base first (l3/ast.ts `index.baseOrdered`, from raise/globalshape.ts).
|
|
341
|
+
*
|
|
342
|
+
* NOT "every access", whatever `Collected.ordered`'s `&&` looks like it enforces. The licence
|
|
343
|
+
* admits an access that carries no order fact at all — a scaling in another block is not
|
|
344
|
+
* comparable, so it answers `undefined` rather than `false` — and 4 of the corpus's licensed
|
|
345
|
+
* symbols have one on both symbol-map arms (`kleod:EntityDeathAnimation`'s `gEntityArray` is 11
|
|
346
|
+
* of 28 accesses). Per SYMBOL is the right grain here and not a shortcut:
|
|
347
|
+
* agbcc CSEs the pool word, so one `ldr` is shared by every access of the name and there is one
|
|
348
|
+
* order fact to have. WITHIN ONE STRUCTURING the `&&` therefore decides nothing:
|
|
349
|
+
* `structure/structure.ts`'s `stampOrderedBases` is a post-pass over the finished body that
|
|
350
|
+
* stamps per SYMBOL, so the accesses of one key leave it agreeing. What the `&&` guards is a
|
|
351
|
+
* LATER pass that rebuilt one node and dropped the stamp — a tree whose stamps are MIXED — and
|
|
352
|
+
* what it does there is refuse the key whole rather than half-home it, which no assembly can
|
|
353
|
+
* reach and test/basecse.test.ts pins on a hand-built tree ('a key whose accesses DISAGREE on
|
|
354
|
+
* the stamp is refused whole, never half-homed').
|
|
355
|
+
*
|
|
356
|
+
* On agbcc a base-first order is what a declared array produces (`build_array_ref`'s fork) and
|
|
357
|
+
* what a pointer local's own initializer STATEMENT produces (see raise/globalshape.ts's header
|
|
358
|
+
* for the compile that separates the two), while the inline cast produces the other order — so
|
|
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 variation
|
|
361
|
+
* off those targets. */
|
|
362
|
+
orderLicensed: boolean;
|
|
149
363
|
}
|
|
150
364
|
|
|
151
365
|
/** The admission rules. NONE is sound, and that is a property of the pass rather than an oversight:
|
|
@@ -156,12 +370,25 @@ export interface BaseKey {
|
|
|
156
370
|
* in a register across the loop too — but hoisting to the FUNCTION TOP forces a callee-saved
|
|
157
371
|
* register, which can add the prologue push/pop the original avoided. `l3/scopebase.ts` is the
|
|
158
372
|
* scope-aware hoist that serves those instead. */
|
|
373
|
+
const reachedOnce = (c: BaseKey): boolean => c.uses < 2;
|
|
374
|
+
|
|
159
375
|
export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
376
|
+
{
|
|
377
|
+
// FIRST, so a cast base attributes here rather than to whichever use-count rule it also trips.
|
|
378
|
+
// Every table derived from this one inherits it EXCEPT `ORDERBASE_GATES`, which ablates it — so
|
|
379
|
+
// a cast base is collectable under every table and bound by exactly one roster family.
|
|
380
|
+
// Censused at `decompile()`'s default structuring, map-less, one tree per row over the
|
|
381
|
+
// artifact's 404 agbcc rows: `ORDERBASE_GATES` admits 12 keys on 10 rows, 11 of them cast keys.
|
|
382
|
+
id: 'cast-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
|
+
sound: false,
|
|
385
|
+
rejects: (c) => c.castBase,
|
|
386
|
+
},
|
|
160
387
|
{
|
|
161
388
|
id: 'single-use',
|
|
162
|
-
why: '
|
|
389
|
+
why: 'a base accessed once is as cheap to load again as to hold in a named local',
|
|
163
390
|
sound: false,
|
|
164
|
-
rejects:
|
|
391
|
+
rejects: reachedOnce,
|
|
165
392
|
},
|
|
166
393
|
{
|
|
167
394
|
id: 'loop',
|
|
@@ -170,45 +397,426 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
170
397
|
rejects: (c) => c.inLoop,
|
|
171
398
|
},
|
|
172
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.
|
|
173
419
|
id: 'repeated-const-offset',
|
|
174
|
-
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',
|
|
175
421
|
sound: false,
|
|
176
422
|
rejects: (c) => c.repeatedConstOffset,
|
|
177
423
|
},
|
|
178
424
|
];
|
|
179
425
|
|
|
180
|
-
|
|
181
|
-
|
|
426
|
+
/** `/basefold`'s admission (rank.ts): the default rules with `single-use` EXEMPTING a base whose
|
|
427
|
+
* offset survived into the memory operand (`unfoldedOffset`, see the header). A separate table
|
|
428
|
+
* rather than a relaxed `single-use`, because the evidence is not proof — an inline
|
|
429
|
+
* aggregate-member access emits the same bytes — so the spelling it generates belongs beside the
|
|
430
|
+
* inline one with the differ between them, never committed on the single-shot path where nothing
|
|
431
|
+
* referees.
|
|
432
|
+
*
|
|
433
|
+
* HOW TO PRICE THE TWO RULES. `without(BASECSE_GATES, 'single-use')` ablates the use-count rule
|
|
434
|
+
* alone. The EXEMPTION has no ablation of its own: ablation removes a whole gate, and this table's
|
|
435
|
+
* gate carries the rule AND its exemption in one `rejects`, so
|
|
436
|
+
* `without(BASEFOLD_GATES, 'single-use-unfolded')` is `without(BASECSE_GATES, 'single-use')` gate
|
|
437
|
+
* object for gate object — the naive full ablation, which costs a real match. What the exemption
|
|
438
|
+
* added is the two tables' admitted-set DIFF, `admittedBases(sfn, BASEFOLD_GATES)` minus
|
|
439
|
+
* `admittedBases(sfn, BASECSE_GATES)`.
|
|
440
|
+
*
|
|
441
|
+
* rank.ts offers the row only where the target declares
|
|
442
|
+
* `compilerBehaviors.foldsConstAddrOffset` — MIPS and PPC put the addend in the instruction by
|
|
443
|
+
* construction (`lui`/`%lo`, `lis`/`ori`), so a surviving offset carries no information there.
|
|
444
|
+
* It offers it at BOTH placements (l3/hoist.ts): a base reached once is loaded where it is used,
|
|
445
|
+
* so where the init sits is the question the differ has to settle, not whether it exists. */
|
|
446
|
+
export const BASEFOLD_GATES: readonly Gate<BaseKey>[] = [
|
|
447
|
+
{
|
|
448
|
+
id: 'single-use-unfolded',
|
|
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',
|
|
450
|
+
sound: false,
|
|
451
|
+
rejects: (c) => reachedOnce(c) && !c.unfoldedOffset,
|
|
452
|
+
},
|
|
453
|
+
...ablateHeuristic(BASECSE_GATES, 'single-use'),
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
/** The `/livebase` variation's admission (rank.ts): the default rules with both PLACEMENT heuristics
|
|
457
|
+
* ablated, keeping only `single-use`. `loop` and `repeated-const-offset` predict which spelling
|
|
458
|
+
* the compiler chose, and both predictions have a counterexample — an MMIO poll (`p[2] = go;
|
|
459
|
+
* while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register the whole
|
|
460
|
+
* time. Neither gate is `sound`, so ablating them can only change which spelling wins, never
|
|
461
|
+
* what a candidate means; the differ referees. */
|
|
462
|
+
export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
|
|
463
|
+
ablateHeuristic(BASECSE_GATES, 'loop'),
|
|
464
|
+
'repeated-const-offset',
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
/** `/livebase-block`'s admission (rank.ts): `/livebase` plus `single-cell`. The two tables differ
|
|
468
|
+
* by exactly one gate, so `without(LIVEBASE_BLOCK_GATES, 'single-cell')` is `/livebase`'s own
|
|
469
|
+
* admission and this selectivity rule prices by ablation like every other.
|
|
470
|
+
*
|
|
471
|
+
* `single-cell` GENERATES a narrower candidate; it does not classify, and taking it for a compiler
|
|
472
|
+
* fact is the way to misuse it. Its counterexample is in this corpus: `synthetic:sizebound`'s
|
|
473
|
+
* `*(u16 *)0x03001048` is reached at one fixed offset only — this rule rejects it — and binding it
|
|
474
|
+
* is what the differ picks (16 against 36 on the same shape). The rule is legitimate anyway
|
|
475
|
+
* because it never SUBTRACTS a candidate: `/livebase` rides beside it, and the differ referees.
|
|
476
|
+
* Promote it into `BASECSE_GATES` or prune with it and that row pays.
|
|
477
|
+
*
|
|
478
|
+
* WHAT IT IS WORTH, since a generator's price is what its own family WINS and not what its
|
|
479
|
+
* refusal explains. Ablated — this table made equal to `LIVEBASE_GATES`, at which point
|
|
480
|
+
* rank.ts's `sameBases` shadow declines the whole `/livebase-block` family — THREE matches go:
|
|
481
|
+
* `synthetic:dmaflat` MATCH → diff:3, `synthetic:dmapoll` MATCH → diff:12 and
|
|
482
|
+
* `synthetic:mixpoll` MATCH → diff:2.
|
|
483
|
+
*
|
|
484
|
+
* A PRICE IS A CLAIM ABOUT THE WHOLE ROSTER, not about one table, and nothing in the gate order
|
|
485
|
+
* can see it go stale: `bench regression` and `bench diff` measure OUTCOMES, and a rule nobody
|
|
486
|
+
* ablated has no outcome. `synthetic:foldpark` is the standing example — `/unfolded` binds that
|
|
487
|
+
* row's bases set-for-set at `first-use`, so with THIS gate ablated alone the row is still MATCH
|
|
488
|
+
* and brackets nothing; ablate both and it goes diff:6. Re-run every number in this paragraph
|
|
489
|
+
* whenever an admission is added to or removed from the roster.
|
|
490
|
+
* HOW: prefer the edit-free form — import this array and `splice` the gate out of it before the
|
|
491
|
+
* first `enumerateCandidates` call, since the roster holds a reference to this very object. The
|
|
492
|
+
* env-read recipe on BASEFOLD_HOISTS edits files instead, and a tap reverted underneath a
|
|
493
|
+
* running process reports ZEROES rather than crashing, which reads exactly like "the rule never
|
|
494
|
+
* fires"; if you use it, hash the tree either side of the window and quote both hashes.
|
|
495
|
+
*
|
|
496
|
+
* A CENSUS OVER WINNERS' VARIATIONS CANNOT STAND IN FOR THAT — "only a row whose winner carries
|
|
497
|
+
* `/livebase-block` can move" is unsound for the reason rank.ts's `seen` dedup spells out. This
|
|
498
|
+
* table's own census over winners' variations reads 5 rows and read 7 before `/unfolded` shipped, and the
|
|
499
|
+
* two that left differ: `synthetic:foldpark` by RENAME (byte-identical source, MATCH either
|
|
500
|
+
* side), `synthetic:unfoldpark` because its winning SPELLING changed, 402 bytes at diff:9 to 397
|
|
501
|
+
* at MATCH.
|
|
502
|
+
*
|
|
503
|
+
* WHICH ROWS THE ABLATION REACHES, since "found by running it" is only an instruction until
|
|
504
|
+
* someone runs it. Enumeration only, no compiles, both arms — `single-cell` spliced out of this
|
|
505
|
+
* array in process, prototypes only, map-less, over all artifact rows; run twice with the working
|
|
506
|
+
* tree hashed either side and byte-identical both times. THE ARTIFACT HELD 951 ROWS THAT DAY (358
|
|
507
|
+
* agbcc, 593 not) and has grown since: this paragraph is at the earlier scope and is not re-run
|
|
508
|
+
* against the later one, so its per-row fans still stand and its two TOTALS do not.
|
|
509
|
+
* THIRTEEN rows change their distinct-source set, corpus fan 48995 → 42701, and ZERO non-agbcc
|
|
510
|
+
* rows are reached — the arm easiest to skip, because this table sits on the UNCONDITIONAL half of
|
|
511
|
+
* the roster and is offered to ido/kmc/mwcc/gcc272 too. READ THAT POPULATION HONESTLY, since a denominator is a rig
|
|
512
|
+
* artifact until it is broken out: of the 593 non-agbcc rows, 400 enumerate in both arms and NONE
|
|
513
|
+
* of them moves; the other 193 decline at the lift or structure seam and have no fan on either
|
|
514
|
+
* side, so they are vacuous rather than evidence. The claim is over the 400.
|
|
515
|
+
* Seven of the thirteen are the rows this note already names (the three matches above, plus the
|
|
516
|
+
* four re-run below); the other six are `kleod:ConfigureEntityBehavior` (fan 1248 → 864),
|
|
517
|
+
* `kleod:ProcessInputAndUpdateEntities` (23040 → 19200), `kleod:SetupBG3WindowOverlay`
|
|
518
|
+
* (696 → 640), `kleod:UpdateCameraScroll` (15936 → 14272), `kleod:UpdateWorldMapNodeAnim`
|
|
519
|
+
* (216 → 192) and `synthetic:livepark` (32 → 24, a MATCH row). `ConfigureEntityBehavior` and
|
|
520
|
+
* `livepark` keep their published winning source in the ablated fan, so neither outcome nor score
|
|
521
|
+
* can move on them. THE OTHER FOUR ARE UNPRICED, not free: they are real-tier rows whose
|
|
522
|
+
* published spelling is map-FUL, and a map-less enumeration does not contain it in EITHER arm, so
|
|
523
|
+
* this rig cannot say. Do not quote them as unmoved.
|
|
524
|
+
*
|
|
525
|
+
* The rows it is quoted against, all re-run at this commit: `synthetic:sizebound` — the
|
|
526
|
+
* counterexample row above, which the narrow family still helps — goes 8 → 10;
|
|
527
|
+
* `sa3:Sio32MultiLoadIntr`, the one REAL-tier row involved and the reason this gate is not a
|
|
528
|
+
* synthetic-only concern, is 69 either way; and `synthetic:foldpark` and `synthetic:unfoldpark`
|
|
529
|
+
* are MATCH either way. A round promoting this rule into `BASECSE_GATES` prices it against all of
|
|
530
|
+
* them, re-run rather than re-quoted.
|
|
531
|
+
*
|
|
532
|
+
* Why the ACCESS SHAPE and not the address: an MMIO register file and the IWRAM halfword beside
|
|
533
|
+
* it are both numeric constants in the same range. And why the rule is not in `BASECSE_GATES`: it
|
|
534
|
+
* would reject nothing there, being a strict refinement of `repeated-const-offset` — past
|
|
535
|
+
* `single-use`, a base with no variable index and one distinct offset touched it twice. */
|
|
536
|
+
export const LIVEBASE_BLOCK_GATES: readonly Gate<BaseKey>[] = [
|
|
537
|
+
...LIVEBASE_GATES,
|
|
538
|
+
{
|
|
539
|
+
id: 'single-cell',
|
|
540
|
+
why: 'a base reached at one fixed offset reads as a scalar, which the source more often spells inline',
|
|
541
|
+
sound: false,
|
|
542
|
+
rejects: (c) => c.singleCell,
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
/** `/unfolded`'s admission (rank.ts): `/livebase` plus `folded-offset`, which REQUIRES the fold
|
|
547
|
+
* evidence `BASEFOLD_GATES` exempts on. The two tables read one field and ask opposite questions
|
|
548
|
+
* of it — that one exempts `single-use` for a key reached ONCE whose offset survived, this one
|
|
549
|
+
* keeps `single-use` and demands the same survival of a key reached TWICE — so neither is the
|
|
550
|
+
* other relaxed.
|
|
551
|
+
*
|
|
552
|
+
* WHY IT IS A SELECTION AND NOT A WIDENING. `/livebase` and `/livebase-block` ARE one chain:
|
|
553
|
+
* every key the narrow table binds the wide one binds too, so between them a function has only
|
|
554
|
+
* "all the reused bases" and "those minus the scalar cells". A source that parked ONE numeric
|
|
555
|
+
* base and left a second one inline is at neither end of that chain, and `singleCell` cannot
|
|
556
|
+
* separate the two — both are reached at fixed offsets. `unfoldedOffset` can: on a compiler that
|
|
557
|
+
* folds a constant subscript into the literal it materializes, the offset that reached the
|
|
558
|
+
* instruction is the one a POINTER LOCAL strode, and the address that reached the pool with the
|
|
559
|
+
* offset already in it is the one the source spelled inline.
|
|
560
|
+
*
|
|
561
|
+
* BUT THIS TABLE IS NOT ITSELF ON THAT CHAIN, and reading it as "the middle one" is wrong in
|
|
562
|
+
* both directions. `singleCell` and `unfoldedOffset` are independent fields, so this table and
|
|
563
|
+
* `LIVEBASE_BLOCK_GATES` are lattice-INCOMPARABLE: a block walk with no constant displacement
|
|
564
|
+
* (`varIndexed`, so not `singleCell`; offset 0, so no `unfoldedOffset`) is bound by that table
|
|
565
|
+
* and refused by this one, and a scalar cell at a surviving offset is the reverse. Censused at
|
|
566
|
+
* `decompile()`'s default structuring, map-less, over the 363 agbcc rows the artifact carried
|
|
567
|
+
* when the census ran (23 unstructurable): 8 keys on 7 functions go to `/livebase-block` alone and 5 keys on 5 functions
|
|
568
|
+
* to this table alone, and `kleod:ProcessInputAndUpdateEntities` crosses BOTH ways on one tree.
|
|
569
|
+
* That is why `sameBases` compares sets and never subsets (see COVERAGE in the header).
|
|
570
|
+
*
|
|
571
|
+
* The evidence is not proof and the gate never treats it as such — what it produces is a
|
|
572
|
+
* candidate beside the other admissions, refereed by the differ. Two counterexamples, both
|
|
573
|
+
* compiled rather than argued:
|
|
574
|
+
* • an aggregate member emits the same operand offset as a strided pointer local;
|
|
575
|
+
* • so does a SYMBOL BASE whose address the tree also uses as a value. agbcc CSEs the symbol
|
|
576
|
+
* reference where it re-materializes the integer, so `sink((int)(u8 *)&gS); sink(((u8 *)&gS)
|
|
577
|
+
* [3]); sink(((u8 *)&gS)[4]);` and its pointer-local twin emit BYTE-IDENTICAL Thumb — one
|
|
578
|
+
* `.word gS`, `add r0, r4, #0`, `ldrb [r4, #0x3]`, `ldrb [r4, #0x4]`. On such a key the
|
|
579
|
+
* field is not weak evidence, it is none. At the scope above, FOUR of this table's 43
|
|
580
|
+
* admissions sit on one: `ProcessInputAndUpdateEntities` gBgDataPtrs / gCallbackQueue /
|
|
581
|
+
* gUnk_03004C20 and `sa3:sub_802DFC8` gStageData. It costs fan and a tie-break, never
|
|
582
|
+
* meaning, because the inline spelling rides beside it.
|
|
583
|
+
* Absence is not evidence either way — see the header for the THREE ways to get one, including
|
|
584
|
+
* the displacement the instruction could not encode, where this gate is blind rather than
|
|
585
|
+
* unpersuaded — so `folded-offset` refuses on MISSING EVIDENCE and never on a claim that the
|
|
586
|
+
* offset was folded.
|
|
587
|
+
*
|
|
588
|
+
* This table is its own gate object, so `without(UNFOLDED_GATES, 'folded-offset')` is
|
|
589
|
+
* `LIVEBASE_GATES` and the rule prices by ablation — the handle `BASEFOLD_GATES`' exemption
|
|
590
|
+
* cannot have (see the note above it). */
|
|
591
|
+
export const UNFOLDED_GATES: readonly Gate<BaseKey>[] = [
|
|
592
|
+
...LIVEBASE_GATES,
|
|
593
|
+
{
|
|
594
|
+
id: 'folded-offset',
|
|
595
|
+
why: 'no offset survived into the operand, so nothing says a pointer local strode this base',
|
|
596
|
+
sound: false,
|
|
597
|
+
rejects: (c) => !c.unfoldedOffset,
|
|
598
|
+
},
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
/** `/orderbase`'s admission (rank.ts): the two rules ABOUT THE BASE ablated — `cast-base` and
|
|
602
|
+
* `single-use` — and one rule demanding the ORDER LICENCE in their place.
|
|
603
|
+
*
|
|
604
|
+
* THE EVIDENCE IS THE ASSEMBLY, and it is the only thing here that is. Every other table on this
|
|
605
|
+
* roster predicts which spelling the source wrote from the SHAPE of the accesses — how many, at
|
|
606
|
+
* what offsets, inside a loop or not — and each prediction has a counterexample this file names.
|
|
607
|
+
* This one reads the instruction ORDER (raise/globalshape.ts's header carries the compiles behind
|
|
608
|
+
* it, and why a declared array and a pointer LOCAL reach the same order by different routes).
|
|
609
|
+
* Compiled through the benchmark's own agbcc command, against a base-first object:
|
|
610
|
+
*
|
|
611
|
+
* extern u16 gTbl[]; gTbl[i] 0 ┐ the same object
|
|
612
|
+
* u16 *p = (u16 *)&gTbl; p[i] 0 ┘
|
|
613
|
+
* ((u16 *)&gTbl)[i] 2 a different one
|
|
614
|
+
*
|
|
615
|
+
* A DERIVED DECLARATION IS NOT THE SAME AS A BARE SPELLING, which is the thing to know before
|
|
616
|
+
* reading the population: where `raise/globalshape.ts` shapes a name the structurer usually spells
|
|
617
|
+
* it bare and no key exists here at all — but a shape is ONE element type for the whole name, so an
|
|
618
|
+
* access that strides something else keeps its cast and its key. `kleod:SetupBG3WindowOverlay`'s
|
|
619
|
+
* `gBgInfo` (a row retired 2026-09-13) derived `elemSize 4` and still reached this table at stride
|
|
620
|
+
* 28, in both arms.
|
|
621
|
+
*
|
|
622
|
+
* What this table admits, censused over the artifact's 370 agbcc rows: map-less 8 rows / 10 keys,
|
|
623
|
+
* map-ful 10 rows / 12 keys. TWO shapes, and the arms differ:
|
|
624
|
+
*
|
|
625
|
+
* • the STRUCT ELEMENT — no `intType`, members read at a displacement — 9 keys map-less, 10
|
|
626
|
+
* map-ful, and the only shape `cast-base`'s ablation reaches. The map-ful extra is
|
|
627
|
+
* `kleod:StreamCmd_SetBGScroll`'s `gBgInfo`, a pool word the map-less lift leaves NUMERIC: the
|
|
628
|
+
* map is what makes it a named global, not anything the licence read from the map.
|
|
629
|
+
* • a PLAIN SCALAR LEAF with no cast anywhere, which only `single-use`'s ablation admits — and a
|
|
630
|
+
* reader deciding whether `single-use` can be put back needs it named. Both inhabitants reach
|
|
631
|
+
* this table for a reason that is NOT an interior read. `kleod:UpdateCameraScroll`'s
|
|
632
|
+
* `gSineTable` (both arms) is refused a declaration on `interior-or-non-access`'s NON-ACCESS
|
|
633
|
+
* half: one clean load, and the same element address feeding three other `add`s.
|
|
634
|
+
* `pokeemerald:Sin2`'s `gSineDegreeTable` (map-ful only) is refused nothing — it DERIVES
|
|
635
|
+
* `elemSize 2` unsigned, and map-less that is what it is spelled as, so there is no key. The
|
|
636
|
+
* symbol map declares the same element SIGNED, map-first wins, and an unsigned load through a
|
|
637
|
+
* signed declaration cannot be spelled bare, so the cast comes back and with it the key.
|
|
638
|
+
*
|
|
639
|
+
* WHY `single-use` GOES. The rule's theory is that one access re-materializes as cheaply as a
|
|
640
|
+
* named local, which is a guess about the source in the absence of evidence; here there is
|
|
641
|
+
* evidence, and `synthetic:bgarr` is a one-access function whose target loads the pool word first.
|
|
642
|
+
* The licence is its OWN gate rather than an exemption folded into a relaxed `single-use`, so
|
|
643
|
+
* `without(ORDERBASE_GATES, 'order-licensed')` prices it — the handle `BASEFOLD_GATES`' fused
|
|
644
|
+
* exemption cannot have.
|
|
645
|
+
*
|
|
646
|
+
* `loop` and `repeated-const-offset` STAY. Neither is about the base's identity and both are fan
|
|
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
|
|
649
|
+
* reader to find: a licensed base with a use inside a loop is admitted by NO table on the roster —
|
|
650
|
+
* this one refuses it on `loop`, and every table that ablates `loop` refuses it on `cast-base` or
|
|
651
|
+
* `single-use` — which is the "a base set that is no row's stays unreachable" debt
|
|
652
|
+
* docs/level-tower.md books against a roster of hand-picked subsets. Measured over the artifact's
|
|
653
|
+
* 370 agbcc rows in both symbol-map arms, `admittedBases(sfn, without(ORDERBASE_GATES, 'loop'))`
|
|
654
|
+
* minus everything any shipped table admits is 0 rows, so the hole is structural and unpopulated.
|
|
655
|
+
* Folding the licence into `cast-base` and `single-use` as an EXEMPTION would close it and reach
|
|
656
|
+
* every table, at the cost of the ablation handle below — the trade `BASEFOLD_GATES`' fused
|
|
657
|
+
* exemption already made once, and not one to make for a class with no inhabitant. rank.ts offers this row only where
|
|
658
|
+
* `compilerBehaviors.arrayShapeFromStride` — the same opt-in the licence itself carries, because
|
|
659
|
+
* the fork is agbcc's and no other compiler has been shown to make it.
|
|
660
|
+
*
|
|
661
|
+
* ITS REACH IS A FUNCTION OF THE SYMBOL MAP, and unevenly so — a sweep is evidence about the arm
|
|
662
|
+
* and the subtree it ran over, so both are stated. Over every cleanly-lifting function of each
|
|
663
|
+
* project's WHOLE `asm` tree, `asm/matchings` included:
|
|
664
|
+
* klonoa binds 18 of 284 map-ful and ZERO map-less, sa3 30 of 1709 map-ful and 29 map-less.
|
|
665
|
+
* The klonoa collapse is THIS GATE and not a missing base: ablating `order-licensed` admits 118
|
|
666
|
+
* map-less klonoa functions where the full table admits none — so the bases are collected and it
|
|
667
|
+
* is the ORDER EVIDENCE that map-less klonoa does not carry. Why the evidence depends on the map is not
|
|
668
|
+
* attributed here; instrument `collect`'s `ordered` before assuming. What follows for a reader
|
|
669
|
+
* either way: a klonoa row carrying no symbol map exercises nothing in this table, and its whole
|
|
670
|
+
* family — the `scope` placement included — is carried on that project by the map-ful arm alone. */
|
|
671
|
+
export const ORDERBASE_GATES: readonly Gate<BaseKey>[] = [
|
|
672
|
+
...ablateHeuristic(ablateHeuristic(BASECSE_GATES, 'cast-base'), 'single-use'),
|
|
673
|
+
{
|
|
674
|
+
id: 'order-licensed',
|
|
675
|
+
why: 'nothing in the assembly says this base was held in a local: the index was scaled first, or the order says nothing',
|
|
676
|
+
sound: false,
|
|
677
|
+
rejects: (c) => !c.orderLicensed,
|
|
678
|
+
},
|
|
679
|
+
];
|
|
680
|
+
|
|
681
|
+
/** The census without the rewrite, so a caller choosing between admissions can compare what two
|
|
682
|
+
* tables would bind for one tree walk each. */
|
|
683
|
+
export function admittedBases(sfn: SFn, gates: readonly Gate<BaseKey>[]): readonly string[] {
|
|
684
|
+
return admit(sfn, gates).keys;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** What each key NAMES, for a caller that has to translate a key into another pass's vocabulary —
|
|
688
|
+
* `l3/scopebase.ts` spells an `addr` base's identity differently, so a string compare across the
|
|
689
|
+
* two would silently never match. Every key the tree holds, admitted or not. */
|
|
690
|
+
export function baseSites(sfn: SFn): ReadonlyMap<string, { base: HoistableBase; width: number; signed: boolean }> {
|
|
691
|
+
return collectFrom(sfn).meta;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** The whole census for one tree — every field seeded empty and one `collect` walk over the body.
|
|
695
|
+
* ONE constructor, because `baseSites` and `admit` must judge the same census: a field added to
|
|
696
|
+
* `Collected` and seeded in only one of two copies reads as an empty tally rather than as a type
|
|
697
|
+
* error. */
|
|
698
|
+
function collectFrom(sfn: SFn): Collected {
|
|
699
|
+
const c: Collected = {
|
|
700
|
+
count: new Map(),
|
|
701
|
+
order: [],
|
|
702
|
+
meta: new Map(),
|
|
703
|
+
inLoop: new Set(),
|
|
704
|
+
constOffCount: new Map(),
|
|
705
|
+
varIndexed: new Set(),
|
|
706
|
+
operandOff: new Set(),
|
|
707
|
+
ordered: new Map(),
|
|
708
|
+
};
|
|
182
709
|
collect(sfn.body, c, false);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
710
|
+
return c;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** The keys `gates` admits, in first-use order, with the census they were judged from. */
|
|
714
|
+
function admit(sfn: SFn, gates: readonly Gate<BaseKey>[]): { c: Collected; keys: string[] } {
|
|
715
|
+
const c = collectFrom(sfn);
|
|
716
|
+
const keys = c.order.filter((k) => {
|
|
717
|
+
const offsets = c.constOffCount.get(k);
|
|
718
|
+
return (
|
|
719
|
+
firstRejection(gates, {
|
|
187
720
|
key: k,
|
|
188
|
-
uses: count.get(k) ?? 0,
|
|
721
|
+
uses: c.count.get(k) ?? 0,
|
|
189
722
|
inLoop: c.inLoop.has(k),
|
|
190
|
-
repeatedConstOffset: [...(
|
|
191
|
-
|
|
192
|
-
|
|
723
|
+
repeatedConstOffset: [...(offsets?.values() ?? [])].some((n) => n >= 2),
|
|
724
|
+
singleCell: !c.varIndexed.has(k) && (offsets?.size ?? 0) <= 1,
|
|
725
|
+
unfoldedOffset: c.operandOff.has(k),
|
|
726
|
+
castBase: c.meta.get(k)!.base.k === 'cast',
|
|
727
|
+
orderLicensed: c.ordered.get(k) === true,
|
|
728
|
+
}) === null
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
return { c, keys };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** THE TWO FLAT PLACEMENTS ALWAYS ANSWER. Both put the run in the top-level statement list, so
|
|
735
|
+
* there is always a tree to return and the caller has no decline to handle — which is why
|
|
736
|
+
* `pipeline.ts` can commit one of them.
|
|
737
|
+
*
|
|
738
|
+
* `scope` DECLINES, and the overload is how a caller is told: `null` means the placement had
|
|
739
|
+
* nothing to say about this function, because no init landed inside a nested list. That tree is
|
|
740
|
+
* byte-for-byte the `first-use` spelling (l3/hoist.ts's `nested`), and the roster withholds the
|
|
741
|
+
* `first-use` row for this table deliberately (rank.ts, ORDERBASE_HOISTS) — so returning it
|
|
742
|
+
* ships the withheld candidate under the scoped row's name.
|
|
743
|
+
*
|
|
744
|
+
* IT WITHDRAWS A SPELLING RATHER THAN COLLAPSING A DUPLICATE, which is what the decline costs.
|
|
745
|
+
* `ORDERBASE_HOISTS` holds exactly two rows, `head` and `scope`, so nothing is ever enumerated
|
|
746
|
+
* at `first-use` for this table and the refused tree has no twin to fold into — its shape and
|
|
747
|
+
* `/volatile` compositions go with it. Over each project's whole `asm` tree, map-ful: of the 48
|
|
748
|
+
* functions `ORDERBASE_GATES` admits, 7 place an init inside a nested list and 41 do not, and for
|
|
749
|
+
* 29 of the 41 the refused spelling is one the `head` row does not already produce. Instrumented
|
|
750
|
+
* on two of those, both map-ful — the `kleod:StreamCmd_SetBGScroll` row (fan 11), and
|
|
751
|
+
* `sub_808A4EC` in the sa3 checkout, which is no row here (fan 40) — where the `head` source is
|
|
752
|
+
* among the candidates `enumerateCandidates` returns and the withheld `first-use` source is not. */
|
|
753
|
+
export function hoistBaseLocals(sfn: SFn, gates?: readonly Gate<BaseKey>[], placement?: 'head' | 'first-use'): SFn;
|
|
754
|
+
export function hoistBaseLocals(sfn: SFn, gates: readonly Gate<BaseKey>[], placement: HoistPlacement): SFn | null;
|
|
755
|
+
export function hoistBaseLocals(
|
|
756
|
+
sfn: SFn,
|
|
757
|
+
gates: readonly Gate<BaseKey>[] = BASECSE_GATES,
|
|
758
|
+
placement: HoistPlacement = 'head',
|
|
759
|
+
): SFn | null {
|
|
760
|
+
const { c, keys: hoisted } = admit(sfn, gates);
|
|
761
|
+
const { meta } = c;
|
|
193
762
|
if (hoisted.length === 0) {
|
|
194
|
-
return sfn;
|
|
763
|
+
return placement === 'scope' ? null : sfn;
|
|
195
764
|
}
|
|
196
765
|
|
|
197
766
|
const fresh = nameAllocator(sfn);
|
|
198
767
|
|
|
199
768
|
const localFor = new Map<string, string>();
|
|
200
769
|
const newLocals: { name: string; type: IrType }[] = [];
|
|
201
|
-
const hoistStmts:
|
|
770
|
+
const hoistStmts: BaseInit[] = [];
|
|
202
771
|
for (const k of hoisted) {
|
|
203
772
|
const m = meta.get(k)!;
|
|
204
|
-
|
|
773
|
+
// A CAST base already wears the pointer type its element needs, so the local takes that type
|
|
774
|
+
// and the cast the structurer wrote becomes the initializer unchanged.
|
|
775
|
+
const ptrType = m.base.k === 'cast' ? m.base.to : T.ptr(scalarTypeForAccess(m.width, m.signed));
|
|
205
776
|
const nm = fresh();
|
|
206
777
|
localFor.set(k, nm);
|
|
207
778
|
newLocals.push({ name: nm, type: ptrType });
|
|
208
779
|
// `p = (T *)base` — the cast makes the local the access's pointer type so each `p[i]` strides it.
|
|
209
|
-
hoistStmts.push({
|
|
780
|
+
hoistStmts.push({
|
|
781
|
+
k: 'assign',
|
|
782
|
+
name: nm,
|
|
783
|
+
value: m.base.k === 'cast' ? m.base : { k: 'cast', to: ptrType, e: m.base },
|
|
784
|
+
});
|
|
210
785
|
}
|
|
211
786
|
|
|
212
|
-
const
|
|
213
|
-
|
|
787
|
+
const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor)));
|
|
788
|
+
// The new inits join the tree's LEADING run of base inits rather than being prepended above it:
|
|
789
|
+
// when rank's /livebase re-runs this pass the head already carries the default run's, and
|
|
790
|
+
// blindly prepending would spell the new base's pool load above locals the compiler loads first.
|
|
791
|
+
// That is why `HoistPlacement` has no `prepend` — the hazard is typed out rather than warned
|
|
792
|
+
// about. `placement` then answers where the whole run goes (l3/hoist.ts, the mechanism
|
|
793
|
+
// sinkinit.ts's policy shares). Under either value it is ordered by first use, which is
|
|
794
|
+
// pool-load order (see `collect`) — deliberately reaching the single default run too (a head of
|
|
795
|
+
// user pointer inits before a firing hoist), where it repairs the same invariant.
|
|
796
|
+
const locals = [...sfn.locals, ...newLocals];
|
|
797
|
+
// The shell carries the minted DECLARATIONS and the REWRITTEN statements together: first-use
|
|
798
|
+
// would not know the new names without the first, and would query the pre-rewrite accesses
|
|
799
|
+
// without the second.
|
|
800
|
+
const { body, moved, nested } = placeBaseLocals({ ...sfn, locals, body: rewritten }, hoistStmts, placement);
|
|
801
|
+
// DEGENERATE SCOPE IS A DECLINE, not a candidate: nothing reached a nested list, so this is the
|
|
802
|
+
// `first-use` tree, which this function's header prices.
|
|
803
|
+
if (placement === 'scope' && nested.length === 0) {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
const out = { ...sfn, body, locals };
|
|
807
|
+
// The two FLAT placements can only put the run in the top-level list, above every use of it by
|
|
808
|
+
// construction. `scope` puts an init inside a nested list, which is where a placing variation can ship
|
|
809
|
+
// the one failure the byte differ rewards — a read of a local whose assignment does not reach it —
|
|
810
|
+
// so the tree it emits is checked rather than argued (contracts.ts).
|
|
811
|
+
//
|
|
812
|
+
// THE POPULATION IS THE MOTION, and `moved` is what the placer says it moved rather than what this
|
|
813
|
+
// function minted. The leading run this pass inherits is the DEFAULT hoist's, committed by
|
|
814
|
+
// `structureChecked` before rank's variations see the tree (pipeline.ts), and `scope` moves those
|
|
815
|
+
// inits too — so `newLocals` names less than half of what has to be judged. Real inhabitants, in the
|
|
816
|
+
// CHECKOUTS rather than in a benchmark row — `DecompressAndLoadLevel` in klonoa and `sub_8052474`
|
|
817
|
+
// in sa3, both map-ful — each sink one inherited `p0` beside the minted `p1`.
|
|
818
|
+
if (placement === 'scope') {
|
|
819
|
+
assertHoistsDominate(out, new Set([...newLocals.map((l) => l.name), ...moved]));
|
|
820
|
+
}
|
|
821
|
+
return out;
|
|
214
822
|
}
|