@asmlift/core 0.5.0 → 0.6.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 +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -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 +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -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/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- 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 +113 -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 +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- 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 +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -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 +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
// asmlift — ARRAY SHAPE FOR A GLOBAL NO SYMBOL MAP DESCRIBES, derived from the assembly's own
|
|
2
|
+
// stride evidence.
|
|
3
|
+
//
|
|
4
|
+
// WHAT IS MISSING WITHOUT THIS. asmlift can already spell a named global's element access two
|
|
5
|
+
// ways: `((u16 *)&gTbl)[i]` (always available, valid under any declaration) and the bare
|
|
6
|
+
// `gTbl[i]` (only when a SYMBOL MAP declares `gTbl` an array — structure/globalaccess.ts
|
|
7
|
+
// `bareArrayElement`). Map-less, only the first exists. On agbcc the two are DIFFERENT OBJECTS,
|
|
8
|
+
// and the difference is visible in the input assembly:
|
|
9
|
+
//
|
|
10
|
+
// gTbl[i] ldr r1, .L3 ; lsl r0, r0, #1 ; add r0, r0, r1 ; ldrh <- BASE first
|
|
11
|
+
// ((u16 *)gTbl)[i] lsl r0, r0, #1 ; ldr r1, .L3 ; add r0, r0, r1 ; ldrh <- INDEX first
|
|
12
|
+
//
|
|
13
|
+
// The mechanism is agbcc's own `build_array_ref` (gcc/c-typeck.c), which forks on
|
|
14
|
+
// `TREE_CODE (TREE_TYPE (array)) == ARRAY_TYPE && TREE_CODE (array) != INDIRECT_REF`: an
|
|
15
|
+
// array-typed OBJECT expands its base ahead of the subscript, every other base takes the pointer
|
|
16
|
+
// path and is expanded last. So the instruction order is EVIDENCE about how the source spelled
|
|
17
|
+
// the base, and this module reads it.
|
|
18
|
+
//
|
|
19
|
+
// THE LICENCE IS THE ASSEMBLY, NEVER A PREFERENCE. Deriving a shape here changes the DEFAULT
|
|
20
|
+
// spelling of every access to that symbol (it does not add a candidate), so a shape is minted
|
|
21
|
+
// only where the asm says the source subscripted a declared array, and the fallback everywhere
|
|
22
|
+
// else is the cast form — byte-identical under any declaration. Two independent kinds of
|
|
23
|
+
// evidence license it, and they answer at different element widths:
|
|
24
|
+
//
|
|
25
|
+
// ORDER — the pool load precedes every scaling of the index, in EVERY access of the name. agbcc
|
|
26
|
+
// CSEs the pool word, so a function that subscripts one global twice has ONE `ldr` and both
|
|
27
|
+
// accesses are ordered against it; the earliest scaling is what decides, which is why one
|
|
28
|
+
// index-first access refuses the whole symbol (`index-materialized-first`). Compiled, that is
|
|
29
|
+
// the right reading: `gTbl[i] + ((u16 *)gTbl)[j]` is the SAME object as `gTbl[i] + gTbl[j]`,
|
|
30
|
+
// while `((u16 *)gTbl)[i] + ((u16 *)gTbl)[j]` is a different one — so a mixed function is
|
|
31
|
+
// decided by its first access and nothing after it is observable
|
|
32
|
+
// (cli/test/matching/array-shape-licence.test.ts compiles all three).
|
|
33
|
+
//
|
|
34
|
+
// Observable only at element width > 1: at width 1 there is nothing to scale, so the base
|
|
35
|
+
// `ldr` comes first whatever the source wrote — measured, `extern u8 g[]; g[i]` and
|
|
36
|
+
// `((u8 *)g)[i]` are byte-identical — and the order says nothing.
|
|
37
|
+
// A CONSTANT ON THE INDEX — the address adds a constant to the INDEX at run time while the
|
|
38
|
+
// pool word's relocation addend stays zero. agbcc folds a constant added to any pointer or
|
|
39
|
+
// cast base into that addend (`gcc/explow.c plus_constant_wide`, and gcc/thumb.h's
|
|
40
|
+
// `LEGITIMIZE_ADDRESS` is empty, so nothing splits it back), so a runtime `add` against a bare
|
|
41
|
+
// `.word gSym` is a shape only the array subscript produces. Available at every width.
|
|
42
|
+
//
|
|
43
|
+
// THE ORDER LICENCE HAS TWO CONSUMERS, and they are asking different questions of the same fact.
|
|
44
|
+
// `inferGlobalArrays` asks "how is this name DECLARED", which needs the order licence AND a
|
|
45
|
+
// whole-element subscript to spell. `orderLicensedGlobals` asks only "was the
|
|
46
|
+
// base materialized before the index was scaled", which is what decides whether the address has a
|
|
47
|
+
// HOME — a pointer local `T *p = (T *)&gSym; p[i]` — or is re-derived inline at each access.
|
|
48
|
+
//
|
|
49
|
+
// THE TWO CONSUMERS DO NOT SHARE A MECHANISM — they share an OBSERVABLE, and the difference
|
|
50
|
+
// matters because only one of the two mechanisms is what the opt-in datum below names. Compiled
|
|
51
|
+
// through the benchmark's own agbcc command, at element width 2:
|
|
52
|
+
//
|
|
53
|
+
// A extern u16 gTbl[]; return gTbl[i]; ldr / lsl / add base first
|
|
54
|
+
// B u16 *p = (u16 *)&gTbl; return p[i]; ldr / lsl / add base first
|
|
55
|
+
// C return ((u16 *)&gTbl)[i]; lsl / ldr / add INDEX first
|
|
56
|
+
// F u16 *p; return (p = (u16 *)&gTbl)[i]; lsl / ldr / add INDEX first
|
|
57
|
+
//
|
|
58
|
+
// F is the discriminator. `build_array_ref` takes the same pointer branch for `p` in B and in F —
|
|
59
|
+
// the two differ only in whether the assignment is a SEPARATE STATEMENT — and only B is base-first.
|
|
60
|
+
// So the fork explains A against C, and STATEMENT ORDERING explains B: the initializer is a
|
|
61
|
+
// statement of its own, evaluated before the subscript, and on a compiler with no instruction
|
|
62
|
+
// scheduler that ordering survives into the object. Both roads lead to the same observable, which
|
|
63
|
+
// is why one licence serves both consumers, and the observable is what the licence reads. Two
|
|
64
|
+
// consequences follow: the opt-in datum this module reads is about the FORK alone, so it is
|
|
65
|
+
// narrower than the home consumer's mechanism (target.ts `arrayShapeFromStride` states that
|
|
66
|
+
// under-reach); and `index-materialized-first` is SOUND for the declaration (index-first ⇒ not a
|
|
67
|
+
// declared array, which F does not touch) and only a heuristic for the home (F is a home that
|
|
68
|
+
// compiles index-first), which is why `ORDER_SHAPE_GATES` below owns its rules instead of
|
|
69
|
+
// selecting that one.
|
|
70
|
+
//
|
|
71
|
+
// EVERY CENSUS IN THIS FILE WAS TAKEN OVER ONE ARTIFACT AND IS DATED BY IT: `186360ca`
|
|
72
|
+
// (2026-09-03), whose 370 agbcc rows include 359 that lift. Refusals are retaken through
|
|
73
|
+
// `arrayShapeRefusals`, the licence tables through `orderLicensedGlobals` and `inferGlobalArrays`;
|
|
74
|
+
// a later artifact re-dates them all, and none of them is a rule.
|
|
75
|
+
//
|
|
76
|
+
// So the order half licenses the HOME on its own, for every name the declaration half refuses for a
|
|
77
|
+
// reason that is NOT about the order. Censused in BOTH symbol-map arms; the population narrows
|
|
78
|
+
// twice, so each line says which step it counts:
|
|
79
|
+
//
|
|
80
|
+
// MAP-LESS MAP-FUL
|
|
81
|
+
// licence-only NAMES 10 on 8 rows 11 on 9 rows
|
|
82
|
+
// …their base keys in the L3 tree 10 keys on 8 rows 11 keys on 9 rows
|
|
83
|
+
// 8 cast, 2 plain leaf 9 cast, 2 plain leaf
|
|
84
|
+
// …of those, keys `ORDERBASE_GATES` admits 9 keys on 7 rows 10 keys on 8 rows
|
|
85
|
+
// 8 cast, 1 plain leaf 9 cast, 1 plain leaf
|
|
86
|
+
//
|
|
87
|
+
// THE ARMS DIFFER BY ONE NAME, and not because the licence reads the map — it never does. The map
|
|
88
|
+
// resolves a pool word the map-less lift leaves NUMERIC, so `kleod:StreamCmd_SetBGScroll`'s
|
|
89
|
+
// `gBgInfo` is a named global on one arm and no global at all on the other. Every other name is
|
|
90
|
+
// identical across the two.
|
|
91
|
+
//
|
|
92
|
+
// `interior-or-non-access` is the FIRST refusal of every name in that population, on both arms, and
|
|
93
|
+
// it is the rule's TWO HALVES that make the two shapes rather than one half twice. The STRUCT
|
|
94
|
+
// ELEMENT has no `intType` and reads its members at a displacement — the INTERIOR half;
|
|
95
|
+
// `kleod:EntityItemDrop`'s `gEntity` is a plain scalar leaf refused the same way (stores at +2, +8,
|
|
96
|
+
// +9, +12, +15, +16 and +20). The other leaf is refused on the NON-ACCESS half instead:
|
|
97
|
+
// `kleod:UpdateCameraScroll`'s `gSineTable` is read once, cleanly, at width 2, and its element
|
|
98
|
+
// address is also an operand of three other `add`s, so nothing under that name is at a displacement
|
|
99
|
+
// at all — which is why the ORDER consumer records ONE access there and no null width. Neither
|
|
100
|
+
// shape says any less about the order than a clean access does. `ADDRESS_GATES` below is therefore
|
|
101
|
+
// two halves: the ELEMENT rules, which both consumers ask, and the DECLARATION rule, which only the
|
|
102
|
+
// first does.
|
|
103
|
+
//
|
|
104
|
+
// WHAT REFUSES — and the list is DOWN THERE, not here. The refusals are two `Gate<Ctx>` tables
|
|
105
|
+
// (`ADDRESS_GATES` and `SHAPE_GATES`, below), each rule carrying its own `why` and the test that
|
|
106
|
+
// fails without it, because an enumeration in a header is prose that nothing re-checks and a
|
|
107
|
+
// refusal's attribution has to be asked of `arrayShapeRefusals`. Every rejection falls back to
|
|
108
|
+
// today's cast spelling rather than guessing.
|
|
109
|
+
//
|
|
110
|
+
// One thing about them belongs here rather than on a table entry, because it is about the pair.
|
|
111
|
+
// THE ELEMENT HALF AND THE RANK HALF ARE ONE DERIVATION, not two commits: a multi-stride address
|
|
112
|
+
// spelled through a rank-1 declaration is a FLAT subscript, and compiled against `tblrank2`'s
|
|
113
|
+
// target the flat spelling scores 4 (the other operand order 6) where the declared rank scores 0
|
|
114
|
+
// and the cast form it would replace scores 3. So a shape is minted with every dimension it needs
|
|
115
|
+
// or not at all. On that row a SECOND, independent gate also refuses the flat form —
|
|
116
|
+
// `elementIndex` (structure/globalaccess.ts) divides a residual into elements only when it is
|
|
117
|
+
// already one scaled term, never a sum — so `strides-do-not-nest` is the rule stated where the
|
|
118
|
+
// shape is decided, not the only thing standing between that row and the worse spelling.
|
|
119
|
+
//
|
|
120
|
+
// WHAT THE DERIVATION GIVES UP, stated because it is the fallback's whole value. `((T *)&gSym)[i]`
|
|
121
|
+
// reproduces the target's bytes under ANY declaration of `gSym`; the bare `gSym[i]` means what the
|
|
122
|
+
// declaration says it means. asmlift's answer is therefore the source PLUS the declaration it
|
|
123
|
+
// derived, and the two are only right together — which is why the shape travels out of every entry
|
|
124
|
+
// path (`DecompileResult.assumedSymbols`, `TraceReport.assumedSymbols`, the cli's `[assumed]` and
|
|
125
|
+
// `[declared]` blocks) rather than being applied and forgotten.
|
|
126
|
+
//
|
|
127
|
+
// The element WIDTH is forced by the evidence: a wrong width would have strided differently in the
|
|
128
|
+
// asm, and at width 1 `relocation-addend` covers the remaining case. The element SIGNEDNESS is NOT — it is a
|
|
129
|
+
// PICK, and the two readings are the same object. Compiled through the benchmark's own agbcc
|
|
130
|
+
// command (`-mthumb-interwork -Wimplicit -O2 -fhex-asm -fprologue-bugfix`):
|
|
131
|
+
//
|
|
132
|
+
// u32 f(s32 i) { return (u16)gS[i]; } over extern const s16 gS[]; ldr / lsl / add / ldrh
|
|
133
|
+
// u32 f(s32 i) { return gS[i]; } over extern const u16 gS[]; ldr / lsl / add / ldrh
|
|
134
|
+
//
|
|
135
|
+
// — byte-identical objects. So a project whose own header says `s16` compiles this module's bare
|
|
136
|
+
// spelling to `ldrsh` and gets different bytes, while the cast form it replaced would have been
|
|
137
|
+
// right beside either header. `pokeemerald:Sin2` and `sa3:sa2__sub_8083504` are two real benchmark
|
|
138
|
+
// rows in exactly that position. This is a fitted declaration in declare.ts's sense — a sound
|
|
139
|
+
// ARTIFACT (the decls and the source really do compile to those bytes) and an unsound CLAIM if the
|
|
140
|
+
// decls are hidden — so it is minted, and it is never hidden. What travels out is the narrower
|
|
141
|
+
// set `assumedShapes` (bottom of this file) computes rather than everything derived here: a shape
|
|
142
|
+
// the structurer did not spell bare, and a name the caller's own map described, are not
|
|
143
|
+
// obligations the reader has — see that function for the two corpus rows that prove each half.
|
|
144
|
+
//
|
|
145
|
+
// LEVEL. L1-derived, L2-shaped, L3-consumed: it runs on the LIFTED function, because the fact it
|
|
146
|
+
// needs — the order the compiler materialized the base in — is destroyed by the raising tower
|
|
147
|
+
// (array LEGALIZATION — `recognizeArrays`, raise/arrays.ts, run as pre-recovery.ts's `arrays`
|
|
148
|
+
// step, and NOT the patterns-as-data idiom fold, which cannot state the `1 << shiftImm ==
|
|
149
|
+
// accessWidth` relation the match needs — rewrites `gaddr; shl; add; load` into one `aload` and
|
|
150
|
+
// the two orders become the same IR; `harr` and `arrcast` lift to different IR and recover to
|
|
151
|
+
// byte-identical IR, which is why this cannot live any later). Its output is a name-keyed
|
|
152
|
+
// `SymbolInfo` map, the same shape a symbol map supplies, consumed by
|
|
153
|
+
// `StructureOptions.inferredSymbols` and by the declaration synthesis — and it NEVER claims a
|
|
154
|
+
// name a real map describes, which knows more.
|
|
155
|
+
// That precedence is enforced TWICE, and both are needed: `structure()` asks the map first, and
|
|
156
|
+
// rank.ts DELETES a map-known name from this map before structuring, because the `/raw-globals`
|
|
157
|
+
// arm structures with no map and declares with one (see the filter's own note there).
|
|
158
|
+
//
|
|
159
|
+
// PER-COMPILER. The fork above is agbcc's. Whether ido/kmc/mwcc distinguish the two spellings at
|
|
160
|
+
// all was not measured, so the gate is a `compilerBehaviors` opt-in rather than a universal:
|
|
161
|
+
// a compiler earns it by showing the same compiled divergence.
|
|
162
|
+
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
163
|
+
import type { SFn } from '../l3/ast';
|
|
164
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
165
|
+
import type { SymbolInfo } from '../symbols';
|
|
166
|
+
import type { TargetDescription } from '../target';
|
|
167
|
+
|
|
168
|
+
/** One additive term of an address residual: `v` scaled by `scale`, or a pure constant.
|
|
169
|
+
* `scaleOp` is the op that DID the scaling (a `shl`/`mul`), which is what carries the position
|
|
170
|
+
* the order licence reads; a term at scale 1 has none. */
|
|
171
|
+
interface Term {
|
|
172
|
+
scale: number;
|
|
173
|
+
/** null ⇒ a constant term, whose value is `konst` */
|
|
174
|
+
v: Value | null;
|
|
175
|
+
konst: number;
|
|
176
|
+
scaleOp: Op | null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** One access of a global's address: the byte residual's terms plus how the cell was read.
|
|
180
|
+
* `signed` is the extension the BARE spelling would have to carry — a load's own signedness, and
|
|
181
|
+
* `false` for a store, which is what structure.ts passes when it asks whether the bare form is
|
|
182
|
+
* spellable. Keeping the store's answer in the same set is what makes "every access of this
|
|
183
|
+
* symbol spells bare" a single `size === 1` test. */
|
|
184
|
+
interface Access {
|
|
185
|
+
/** The width of the ELEMENT this access reads whole, or `null` for an INTERIOR access — one at a
|
|
186
|
+
* non-zero displacement, which the order consumer records (`interiorIsEvidence`) and the
|
|
187
|
+
* declaration consumer never sees. Nullable rather than "the load's width" because the two are
|
|
188
|
+
* not the same fact: a `ldrh [r1, #0x10]` two bytes into a 28-byte element evidences the order
|
|
189
|
+
* and nothing whatever about the element, and a rule that read `2` out of it would be reading a
|
|
190
|
+
* fabricated element width. Any rule that wants a width has to spell the null case, which is
|
|
191
|
+
* what keeps a future widening of `ORDER_SHAPE_GATES` a refusal instead of a wrong answer. */
|
|
192
|
+
elementWidth: number | null;
|
|
193
|
+
signed: boolean;
|
|
194
|
+
isLoad: boolean;
|
|
195
|
+
terms: Term[];
|
|
196
|
+
gaddr: Op;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** An access the DECLARATION path recorded: a whole-element read or write, so it evidences an
|
|
200
|
+
* element width and the `null` case does not arise. `accessesBySymbol` runs with
|
|
201
|
+
* `interiorIsEvidence` false there and takes only `isElementAccess` consumers, which is what makes
|
|
202
|
+
* this narrowing a fact about the path rather than an assumption — and it is what lets `shapeOf`
|
|
203
|
+
* read `elemSize` without spelling a case that would be a declaration carrying a sub-word read's
|
|
204
|
+
* width as if it were the element's. */
|
|
205
|
+
interface ElementAccess extends Access {
|
|
206
|
+
elementWidth: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── the refusals, as DATA ────────────────────────────────────────────────────────────────────
|
|
210
|
+
//
|
|
211
|
+
// Two tables, because the decision has two stages with different subjects. `ADDRESS_GATES` runs
|
|
212
|
+
// per USE of the symbol's address and answers "is this an element address at all"; one rejection
|
|
213
|
+
// there refuses the WHOLE symbol, because what is being derived is a DECLARATION. `SHAPE_GATES`
|
|
214
|
+
// then runs once over everything the surviving accesses evidence and answers "does one array
|
|
215
|
+
// declaration describe them all". Both are `Gate<Ctx>` tables (l3/gates.ts) rather than an `||`
|
|
216
|
+
// chain for the reason that file exists: a refusal a pass's soundness rests on has to be
|
|
217
|
+
// ABLATABLE — `without(GATES, id)` re-runs the real predicate on real input — and attributable,
|
|
218
|
+
// so `firstRejection` names the one rule that decided rather than the set that co-occurred.
|
|
219
|
+
//
|
|
220
|
+
// THAT DISTINCTION IS NOT COSMETIC HERE, and the three readings of "what is this rule worth" pull
|
|
221
|
+
// apart. Instrumented over the dated artifact's 359 lifting agbcc functions (7 of which derive a
|
|
222
|
+
// shape), per rule — and each column is a DIFFERENT experiment, which is why they disagree:
|
|
223
|
+
//
|
|
224
|
+
// 1. ON ITS OWN — how many symbols this rule alone would reject, run with the rest of the address
|
|
225
|
+
// table EMPTY, so a symbol another rule reaches first is still counted here.
|
|
226
|
+
// 2. FIRST — how many it is the first rejection of in the SHIPPED order, which is the attribution
|
|
227
|
+
// `arrayShapeRefusals` reports.
|
|
228
|
+
// 3. MOVED — how many functions' derived maps change when this rule alone is removed from the
|
|
229
|
+
// shipped table, which is the only column that prices the rule against the shipped pass.
|
|
230
|
+
//
|
|
231
|
+
// The three columns, in that order (a dated measurement, not an invariant — re-take it through
|
|
232
|
+
// `arrayShapeRefusals` rather than trusting it):
|
|
233
|
+
//
|
|
234
|
+
// address-escapes 141 137 0
|
|
235
|
+
// interior-or-non-access 33 24 1
|
|
236
|
+
// relocation-addend 27 17 0
|
|
237
|
+
// stride-is-not-the-element 25 0 0
|
|
238
|
+
// no-positive-evidence 23 10 8
|
|
239
|
+
// no-subscript 21 0 0
|
|
240
|
+
// residual-not-a-sum 3 2 0
|
|
241
|
+
// mixed-access-width 2 0 0
|
|
242
|
+
// index-materialized-first 1 1 0
|
|
243
|
+
// (the other five) 0 0 0
|
|
244
|
+
//
|
|
245
|
+
// — so on the dated artifact twelve of the fourteen rules change nothing when removed, and the rule
|
|
246
|
+
// the first column nominates loudest is one of them: `address-escapes` rejects 141 symbols, is FIRST
|
|
247
|
+
// for 137, and moves NOTHING. On this corpus the uses it rejects are the symbol's only ones, so
|
|
248
|
+
// with the rule gone they simply contribute no access and the name ends with nothing to shape —
|
|
249
|
+
// same outcome, and the rule's value there is the ATTRIBUTION. It is still `sound`, because a
|
|
250
|
+
// symbol that has a good access ALONGSIDE an escaping one does derive without it, which is a wrong
|
|
251
|
+
// answer and is the fixture the ablation test uses. Co-occurrence is not reach either:
|
|
252
|
+
// `no-subscript` would reject 21 symbols and is first for none of them. That does NOT make the twelve
|
|
253
|
+
// decoration — each is right about a shape, and the ablation test beside this module gives all
|
|
254
|
+
// thirteen rules that any input reaches a fixture that DOES reach them (`address-unused` is the
|
|
255
|
+
// fourteenth and has none), where NINE are the only thing standing between their fixture and a
|
|
256
|
+
// derivation. What it does mean is that "which refusal protects which row" is a question for
|
|
257
|
+
// `arrayShapeRefusals`, never for a comment.
|
|
258
|
+
|
|
259
|
+
/** One USE of a symbol's address, reduced to the facts `ADDRESS_GATES` decide over. */
|
|
260
|
+
interface AddressUse {
|
|
261
|
+
/** the use is the `add` that forms an element address */
|
|
262
|
+
readonly isAdd: boolean;
|
|
263
|
+
/** the add's other operand is a constant — the relocation addend, which the frontend's pool
|
|
264
|
+
* grammar spells out as `add(gaddr, const)` */
|
|
265
|
+
readonly addendIsConst: boolean;
|
|
266
|
+
/** the residual's additive terms, or null when the tree is not a plain sum this walk can read */
|
|
267
|
+
readonly terms: Term[] | null;
|
|
268
|
+
/** every consumer of the computed address: whether each is a whole-element load/store at
|
|
269
|
+
* displacement 0 */
|
|
270
|
+
readonly consumers: readonly { readonly isElementAccess: boolean }[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Is this use of `&gSym` an ELEMENT ADDRESS at all — does the base own every byte the address
|
|
274
|
+
* adds, is the residual readable as a sum, and does anything read the result? The half that asks
|
|
275
|
+
* about the ADDRESS, which is the half BOTH consumers of this module need (see THE ORDER LICENCE
|
|
276
|
+
* HAS TWO CONSUMERS in the header). Order is the attribution: the first rejection is the rule that
|
|
277
|
+
* decided.
|
|
278
|
+
*
|
|
279
|
+
* THESE RULE OBJECTS ARE SHARED BY BOTH CONSUMERS, unlike `ORDER_SHAPE_GATES`', and the reason
|
|
280
|
+
* they may be is that the question really is identical: "does the base own these bytes" has one
|
|
281
|
+
* answer whether the caller goes on to declare an array or to home a pointer. What does NOT carry
|
|
282
|
+
* across is a `sound: true` here — soundness is a claim about the DECLARATION, and for the licence
|
|
283
|
+
* an over-admission costs a candidate. `address-escapes` and `interior-or-non-access` are the two
|
|
284
|
+
* sound entries in this file's address half, and only the second is in the declaration half, so
|
|
285
|
+
* the one shared sound rule is `address-escapes`.
|
|
286
|
+
*
|
|
287
|
+
* A SHARED RULE OBJECT IS NOT SHARED COVERAGE. Every `guardedBy` below names a test in the
|
|
288
|
+
* DECLARATION's suite, and those ablations run `inferGlobalArrays` — which prices a rule against
|
|
289
|
+
* the declaration and says nothing about the licence. The licence has its own per-rule sweep
|
|
290
|
+
* beside this module ('the order licence: which rule decides…'), running `orderLicensedGlobals`
|
|
291
|
+
* with one rule removed on fixtures that REACH it: for an address rule that means a clean
|
|
292
|
+
* base-first access BESIDE the rejected use, since a fixture whose only use is the rejected one
|
|
293
|
+
* records no access either way and prices nothing. All five rules any input reaches measure
|
|
294
|
+
* uniquely load-bearing there; `address-unused` is the sixth and has no reaching fixture in
|
|
295
|
+
* either consumer. */
|
|
296
|
+
export const ELEMENT_ADDRESS_GATES: readonly Gate<AddressUse>[] = [
|
|
297
|
+
{
|
|
298
|
+
id: 'address-escapes',
|
|
299
|
+
why: 'the address is used as something other than the base of one element-address add',
|
|
300
|
+
sound: true,
|
|
301
|
+
guardedBy: 'global-array-shape.test.ts: the address escaping to a callee refuses the whole symbol',
|
|
302
|
+
rejects: (u) => !u.isAdd,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
// ATTRIBUTING, NOT UNIQUELY LOAD-BEARING: with this rule removed, `interior-or-non-access`
|
|
306
|
+
// refuses the same symbol (measured on its fixture — the addend `add`'s result feeds the index
|
|
307
|
+
// `add`, which is not an element access). It is FIRST because it names the real cause — this
|
|
308
|
+
// is the `arrbias` control, whose pool word is `.word gTbl+0x1`.
|
|
309
|
+
id: 'relocation-addend',
|
|
310
|
+
why: 'a constant added straight to the address IS the relocation addend, and belongs to the base',
|
|
311
|
+
sound: false,
|
|
312
|
+
guardedBy: 'global-array-shape.test.ts: a non-zero relocation addend refuses the symbol outright',
|
|
313
|
+
rejects: (u) => u.addendIsConst,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
// Attributing: with it removed the walk stops at the `sub` and records no non-constant term,
|
|
317
|
+
// so `no-subscript` refuses the same symbol (measured on its fixture).
|
|
318
|
+
id: 'residual-not-a-sum',
|
|
319
|
+
why: 'a `sub` makes a term’s sign depend on the walk, and a negative stride is not a subscript',
|
|
320
|
+
sound: false,
|
|
321
|
+
guardedBy: 'global-array-shape.test.ts: a subtracted term in the residual refuses',
|
|
322
|
+
rejects: (u) => u.isAdd && u.terms === null,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'address-unused',
|
|
326
|
+
why: 'a computed element address nothing reads evidences nothing either way',
|
|
327
|
+
sound: false,
|
|
328
|
+
rejects: (u) => u.isAdd && u.consumers.length === 0,
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
/** …and does a WHOLE-ELEMENT SUBSCRIPT spell what the address reaches? The half that is about the
|
|
333
|
+
* DECLARATION, so only `inferGlobalArrays` asks it: `orderLicensedGlobals` is deciding where the
|
|
334
|
+
* base was materialized, a question an interior read answers exactly as well as a whole-element
|
|
335
|
+
* one. */
|
|
336
|
+
export const DECLARATION_ADDRESS_GATES: readonly Gate<AddressUse>[] = [
|
|
337
|
+
{
|
|
338
|
+
// THIS is the rule that decides the `bgarr` shape — a 28-byte element read 2 bytes at a time.
|
|
339
|
+
// It is uniquely load-bearing only where the symbol ALSO has a clean access: with an interior read alone,
|
|
340
|
+
// removing the rule records no access at all (an interior read is not evidence, and is
|
|
341
|
+
// filtered out) and the symbol is refused anyway, while beside a clean access removing it
|
|
342
|
+
// derives an element type off a name one of whose uses this spelling does not model.
|
|
343
|
+
// `kleod:UpdateCameraScroll` is that shape on the corpus — `gSineTable` derives `elemSize 2`
|
|
344
|
+
// without this rule — which is why its fixture carries both accesses. Its rejected use there
|
|
345
|
+
// is the NON-ACCESS half rather than the interior one (the element address is an operand of
|
|
346
|
+
// another `add`, and nothing under that name is read at a displacement), which is a second
|
|
347
|
+
// shape and has a test of its own: 'a non-access use refuses the declaration too'.
|
|
348
|
+
id: 'interior-or-non-access',
|
|
349
|
+
why: 'a non-zero displacement reads an INTERIOR of the element, which a whole-element subscript cannot spell',
|
|
350
|
+
sound: true,
|
|
351
|
+
guardedBy: 'global-array-shape.test.ts: an element read at a displacement INSIDE it keeps the cast spelling',
|
|
352
|
+
rejects: (u) => u.consumers.some((c) => !c.isElementAccess),
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
/** Both halves, ELEMENT first: the one ordered table `inferGlobalArrays` asks, and the order the
|
|
357
|
+
* declaration's attribution is stated in. */
|
|
358
|
+
export const ADDRESS_GATES: readonly Gate<AddressUse>[] = [...ELEMENT_ADDRESS_GATES, ...DECLARATION_ADDRESS_GATES];
|
|
359
|
+
|
|
360
|
+
/** Everything one symbol's surviving accesses evidence, reduced to the facts `SHAPE_GATES` decide
|
|
361
|
+
* over. Per-access fields stay per-access on purpose: a rank is a property of ONE address
|
|
362
|
+
* expression, never of the union of several (see `ranks-disagree`). */
|
|
363
|
+
interface ShapeEvidence {
|
|
364
|
+
/** The three questions the rules below ask of the DISTINCT element widths under this name, and
|
|
365
|
+
* nothing more — a set is not offered, because a rule handed one reads it positionally sooner or
|
|
366
|
+
* later and `widths[0]` is "the FIRST recorded access", never "the only one".
|
|
367
|
+
*
|
|
368
|
+
* AN INTERIOR ACCESS EVIDENCES NO ELEMENT WIDTH. It reads at a non-zero displacement, so the
|
|
369
|
+
* load's own width is the sub-word read's and not the element's — `Access.elementWidth` records
|
|
370
|
+
* it as `null` for exactly that reason, and a rule that read `2` out of a `ldrh` two bytes into
|
|
371
|
+
* a 28-byte element would be reading a fabricated element. Only the ORDER consumer records one
|
|
372
|
+
* (`interiorIsEvidence`); the declaration consumer's own `interior-or-non-access` refuses the
|
|
373
|
+
* whole symbol first.
|
|
374
|
+
*
|
|
375
|
+
* Three named symbols on the dated artifact (see the module note) record a clean access and an
|
|
376
|
+
* interior one under ONE name, in both symbol-map arms — `kleod:EntityDeathAnimation`'s
|
|
377
|
+
* `gEntityArray` and `kleod:EntityItemDrop`'s `gEntity` record the interior one first,
|
|
378
|
+
* `kleod:TransformSingleEntityToScreen`'s `gUnk_03002920` records the clean one first. That last
|
|
379
|
+
* is the inhabitant a positional read gets wrong, which is why this is a summary and not a list.
|
|
380
|
+
* `kleod:UpdateCameraScroll` is NOT one of them: it is the DECLARATION half's worked example
|
|
381
|
+
* (`interior-or-non-access` ablated derives `elemSize 2` there), and in the order consumer its
|
|
382
|
+
* `gSineTable` records no interior access at all. */
|
|
383
|
+
readonly widths: {
|
|
384
|
+
/** more than one distinct width under the name — no single element type to declare. A symbol
|
|
385
|
+
* read ONLY at interiors is NOT mixed, and admitting it is right: "one name, two element
|
|
386
|
+
* types" is not what it violates, and every rule that goes on to want a width refuses. */
|
|
387
|
+
readonly mixed: boolean;
|
|
388
|
+
/** some access reads an INTERIOR, so some access evidences no element width at all */
|
|
389
|
+
readonly hasInterior: boolean;
|
|
390
|
+
/** some access reads a sub-word element — the only widths at which the declared signedness
|
|
391
|
+
* changes the emitted bytes */
|
|
392
|
+
readonly anySubWord: boolean;
|
|
393
|
+
};
|
|
394
|
+
/** distinct extensions, a store's implicit `false` included */
|
|
395
|
+
readonly signs: boolean[];
|
|
396
|
+
readonly perAccess: readonly {
|
|
397
|
+
/** THIS access's element width, or null for an interior read — the per-address counterpart of
|
|
398
|
+
* `widths`, so a rule about one address never reads another's. */
|
|
399
|
+
readonly elementWidth: number | null;
|
|
400
|
+
/** ascending distinct strides of the non-constant terms; `[]` = the address names no subscript */
|
|
401
|
+
readonly strides: number[];
|
|
402
|
+
/** the inner extents those strides nest into, or null when they do not nest */
|
|
403
|
+
readonly extents: number[] | null;
|
|
404
|
+
/** a constant term that is not a whole number of elements — a mid-element displacement */
|
|
405
|
+
readonly midElementConst: boolean;
|
|
406
|
+
/** the order licence for this access (see `baseFirst`) */
|
|
407
|
+
readonly baseFirst: boolean | undefined;
|
|
408
|
+
}[];
|
|
409
|
+
/** some access adds a non-zero constant on the INDEX side */
|
|
410
|
+
readonly constOnIndex: boolean;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** THE TWO ORDER PREDICATES, shared by the declaration table below and by `ORDER_SHAPE_GATES`.
|
|
414
|
+
* Shared as FUNCTIONS rather than as rule OBJECTS: the predicate really is the same question in
|
|
415
|
+
* both places, while `sound`, `why` and the guard are not — and sharing a rule OBJECT would also
|
|
416
|
+
* carry `no-positive-evidence`'s second disjunct, which is not about the order (`ORDER_SHAPE_GATES`
|
|
417
|
+
* states what that admits). */
|
|
418
|
+
const anIndexFirstAccess = (e: ShapeEvidence): boolean => e.perAccess.some((a) => a.baseFirst === false);
|
|
419
|
+
const noOrderEvidence = (e: ShapeEvidence): boolean => !e.perAccess.some((a) => a.baseFirst === true);
|
|
420
|
+
|
|
421
|
+
/** Does ONE array declaration describe every access of this name? Each rejection falls back to
|
|
422
|
+
* `((T *)&gSym)[i]`, which is byte-identical under any declaration. */
|
|
423
|
+
export const SHAPE_GATES: readonly Gate<ShapeEvidence>[] = [
|
|
424
|
+
{
|
|
425
|
+
// Attributing on the fixture below: with it removed `mixed-extension` refuses the same symbol.
|
|
426
|
+
// Measured there, the widths are 2 and 4 and `signs` is `[false, true]`, so the substitute
|
|
427
|
+
// refuses on `anySubWord && signs.length !== 1` — the sub-word read really does disagree about
|
|
428
|
+
// its extension — while `stride-is-not-the-element` DERIVES on that fixture (each access's stride IS
|
|
429
|
+
// its own element width, which is what reading `perAccess[i].elementWidth` buys). Only the order
|
|
430
|
+
// of the two rules keeps the attribution here. It is first because "one name, two element types"
|
|
431
|
+
// is the reason and the extension disagreement is a symptom of it.
|
|
432
|
+
id: 'mixed-access-width',
|
|
433
|
+
why: 'two widths under one name have no single element type to declare',
|
|
434
|
+
sound: false,
|
|
435
|
+
guardedBy: 'global-array-shape.test.ts: two access widths under one name refuse',
|
|
436
|
+
rejects: (e) => e.widths.mixed,
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
// Asked only where it CHANGES the emitted bytes. A 4-byte element extends nothing, so
|
|
440
|
+
// `bareArrayElement` ignores signedness there and a width-4 array both read and written would
|
|
441
|
+
// otherwise refuse for a distinction the compiler cannot see.
|
|
442
|
+
id: 'mixed-extension',
|
|
443
|
+
why: 'the declared element type is the only thing in the emitted C saying how a sub-word read fills',
|
|
444
|
+
sound: true,
|
|
445
|
+
guardedBy: 'global-array-shape.test.ts: one name read signed and unsigned refuses',
|
|
446
|
+
rejects: (e) => e.widths.hasInterior || (e.widths.anySubWord && e.signs.length !== 1),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
// Attributing: an address with no variable term also has no stride, so
|
|
450
|
+
// `stride-is-not-the-element` refuses it (measured on its fixture). It is stated separately
|
|
451
|
+
// because "there is no subscript here at all" and "the subscript scales by the wrong thing"
|
|
452
|
+
// are different facts.
|
|
453
|
+
id: 'no-subscript',
|
|
454
|
+
why: 'an address with no variable term names no element, so it evidences no array',
|
|
455
|
+
sound: false,
|
|
456
|
+
guardedBy: 'global-array-shape.test.ts: an access with no variable term refuses',
|
|
457
|
+
rejects: (e) => e.perAccess.some((a) => a.strides.length === 0),
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: 'stride-is-not-the-element',
|
|
461
|
+
why: 'the innermost stride must BE the element the access reads whole, or the subscript scales by the wrong thing',
|
|
462
|
+
sound: true,
|
|
463
|
+
guardedBy: 'global-array-shape.test.ts: an index pre-scaled past the element refuses',
|
|
464
|
+
rejects: (e) => e.perAccess.some((a) => a.elementWidth === null || a.strides[0] !== a.elementWidth),
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: 'strides-do-not-nest',
|
|
468
|
+
why: 'strides that are not whole multiples of one another are not a declared rank at all',
|
|
469
|
+
sound: true,
|
|
470
|
+
guardedBy: 'global-array-shape.test.ts: a stride that is not a whole multiple of the element refuses entirely',
|
|
471
|
+
rejects: (e) => e.perAccess.some((a) => a.extents === null),
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
// `build_array_ref` recurses once per subscript WITHIN one access, so nesting is a property of
|
|
475
|
+
// a single address. Two accesses at strides 4 and 16 on a flat `extern s32 g[]` (`g[i]` and
|
|
476
|
+
// `g[j * 4]`) are not a rank; unioning them declares `extern s32 g[][4]` and spells `g[0][i]`,
|
|
477
|
+
// a positive claim about the object that the assembly does not make and that no longer
|
|
478
|
+
// compiles against the project's own header.
|
|
479
|
+
id: 'ranks-disagree',
|
|
480
|
+
why: 'a rank is read off ONE address expression; two accesses must agree or there is no one declaration',
|
|
481
|
+
sound: true,
|
|
482
|
+
guardedBy: 'global-array-shape.test.ts: two accesses at different strides are not a rank',
|
|
483
|
+
rejects: (e) => new Set(e.perAccess.map((a) => JSON.stringify(a.extents))).size > 1,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: 'mid-element-constant',
|
|
487
|
+
why: 'a constant that is not a whole number of elements is a displacement no subscript spells',
|
|
488
|
+
sound: true,
|
|
489
|
+
guardedBy: 'global-array-shape.test.ts: a constant that is not a whole element refuses',
|
|
490
|
+
rejects: (e) => e.perAccess.some((a) => a.midElementConst),
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
// ONE index-first access refuses the WHOLE symbol, and that is where this rule is uniquely
|
|
494
|
+
// load-bearing: on a single-access function `no-positive-evidence` would refuse anyway, but a
|
|
495
|
+
// function that subscripts the name twice with only ONE of them index-first has positive
|
|
496
|
+
// evidence and still must not derive. "Index-first" means what `baseFirst` can SEE — a
|
|
497
|
+
// scaling before the pool load in the pool load's own block; a scaling in another block is
|
|
498
|
+
// not an index-first access, it is no evidence, and it belongs to the rule below.
|
|
499
|
+
// Compiled: `gTbl[i] + gTbl[j]` and
|
|
500
|
+
// `gTbl[i] + ((u16 *)gTbl)[j]` are the SAME object, while `((u16 *)gTbl)[i] + ((u16 *)gTbl)[j]`
|
|
501
|
+
// is a different one — and agbcc CSEs the pool word, so the first access's order is the only
|
|
502
|
+
// place that difference shows.
|
|
503
|
+
id: 'index-materialized-first',
|
|
504
|
+
why: 'a scaling of the index precedes the pool load IN ITS OWN BLOCK — the pointer path, which is the cast spelling',
|
|
505
|
+
sound: true,
|
|
506
|
+
// Its SECOND reaching fixture is the compiler-shaped one: `…and the same function cast-spelled
|
|
507
|
+
// still refuses, on the access that CAN be compared` runs the real agbcc output for a function
|
|
508
|
+
// that subscripts the name once outside a loop and once inside it. It is not named in
|
|
509
|
+
// `guardedBy` because that field is matched against a single test title (gate-contract.ts).
|
|
510
|
+
guardedBy: 'global-array-shape.test.ts: one index-first access refuses a symbol the others license',
|
|
511
|
+
rejects: anIndexFirstAccess,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: 'no-positive-evidence',
|
|
515
|
+
why: 'no order fact and no index-side constant: the two spellings are the same object, so a shape would be a guess',
|
|
516
|
+
sound: true,
|
|
517
|
+
guardedBy: 'global-array-shape.test.ts: no evidence at all — width 1, no constant — claims nothing',
|
|
518
|
+
rejects: (e) => noOrderEvidence(e) && !e.constOnIndex,
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
|
|
522
|
+
/** The rules that decide the ORDER LICENCE — `orderLicensedGlobals`' shape half.
|
|
523
|
+
*
|
|
524
|
+
* ITS OWN TWO RULES, sharing the PREDICATES above and nothing else. Selecting `SHAPE_GATES`' pair
|
|
525
|
+
* by id instead is an over-admission rather than a style point:
|
|
526
|
+
* `no-positive-evidence` is a DISJUNCTION — an order fact OR a constant on the index — and
|
|
527
|
+
* only the first disjunct is about the order. A constant on the index evidences a SUBSCRIPT (see
|
|
528
|
+
* the header: agbcc folds a constant added to a pointer or cast base into the relocation addend,
|
|
529
|
+
* so a runtime `add` against a bare `.word gSym` is a shape only the array form produces); it says
|
|
530
|
+
* nothing whatever about where the base was materialized, and the inline cast `((u16 *)&g)[i + 1]`
|
|
531
|
+
* scales that constant exactly as `g[i + 1]` does. Compiled through the benchmark's own agbcc
|
|
532
|
+
* command, `extern u8 gTbl[]; s8 *p = (s8 *)gTbl; return gTbl[i + 1] + p[j];` has no scaling
|
|
533
|
+
* anywhere — every access is width 1 — so `baseFirst` is `undefined` at every access, and the
|
|
534
|
+
* selected table licenses `gTbl` on the constant alone, handing `/orderbase` a home the assembly
|
|
535
|
+
* never evidenced. On the dated artifact (see the module note) the difference between the two
|
|
536
|
+
* tables is ONE name on both symbol-map arms (`synthetic:harridx`'s `gTbl`, licensed by the selection with no
|
|
537
|
+
* access whose `baseFirst` is `true`, and shaped there so the structurer spells it bare); the
|
|
538
|
+
* compiled counterexample is what says the class is not that one row.
|
|
539
|
+
*
|
|
540
|
+
* `sound` IS WHERE THE TWO CONSUMERS PART COMPANY. In `SHAPE_GATES` both rules are sound: what is
|
|
541
|
+
* derived there is a DECLARATION, and a
|
|
542
|
+
* wrong one changes the meaning of every access to the name. Here nothing is declared — the
|
|
543
|
+
* licence only OFFERS `/orderbase` a candidate beside the inline spelling, which the differ then
|
|
544
|
+
* referees — so an over-licensed name costs fan and a tie-break, never meaning, and these two are
|
|
545
|
+
* heuristics. Under-licensing is a lost candidate for the same reason, and this table really does
|
|
546
|
+
* under-license: row F of the header's compile table is a home that comes out INDEX-first, so
|
|
547
|
+
* `order-index-first` declines it. That is the direction the table is allowed to be wrong in, and
|
|
548
|
+
* it is why neither rule is `sound` here.
|
|
549
|
+
*
|
|
550
|
+
* What IS shared is the predicate, so an edit to either reaches both consumers — while each rule
|
|
551
|
+
* object carries its own `sound`, `why` and `guardedBy`, so the guard the contract test checks is
|
|
552
|
+
* one that ablates the rule against THIS consumer rather than against the declaration. */
|
|
553
|
+
export const ORDER_SHAPE_GATES: readonly Gate<ShapeEvidence>[] = [
|
|
554
|
+
{
|
|
555
|
+
// ITS GUARD HAS TO BE THE MIXED SHAPE. On BOTH halves of the minimal pair `base-first licenses,
|
|
556
|
+
// index-first does not` this rule can be removed and the licence is unchanged, because on the
|
|
557
|
+
// index-first half the single access has `baseFirst === false` and `no-order-evidence` — which
|
|
558
|
+
// wants ONE access that says `true` — refuses anyway. The input where the two rules come apart
|
|
559
|
+
// is a name with one index-first access AND one base-first access: there `no-order-evidence` is
|
|
560
|
+
// satisfied and only this rule refuses. On the dated artifact (see the module note) that shape
|
|
561
|
+
// has no inhabitant, and "no inhabitant" is two censuses rather than one inferred from the
|
|
562
|
+
// other, both taken on BOTH symbol-map arms: of the symbols that reach this table (33 map-less, 34 map-ful)
|
|
563
|
+
// 1 has an index-first access and 0 have accesses in both orders, and this rule alone blocks 0
|
|
564
|
+
// names where `no-order-evidence` blocks 13. So it is kept for the class rather than for a row,
|
|
565
|
+
// and the fixture is what shows the class is real: compiled through the benchmark's own agbcc
|
|
566
|
+
// command, `((u16 *)gTbl)[i] + gTbl[j]` is a different object from `gTbl[i] + gTbl[j]` (and the
|
|
567
|
+
// same one as `((u16 *)gTbl)[i] + ((u16 *)gTbl)[j]`), so the first access's order really does
|
|
568
|
+
// decide.
|
|
569
|
+
id: 'order-index-first',
|
|
570
|
+
why: 'a scaling of the index precedes the pool load in its own block — the inline pointer path, which has no home',
|
|
571
|
+
sound: false,
|
|
572
|
+
guardedBy: 'global-array-shape.test.ts: one index-first access refuses a name the licence would otherwise grant',
|
|
573
|
+
rejects: anIndexFirstAccess,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
id: 'no-order-evidence',
|
|
577
|
+
why: 'no access materialized the base before scaling the index, so nothing here says the base had a home',
|
|
578
|
+
sound: false,
|
|
579
|
+
guardedBy: 'global-array-shape.test.ts: a constant on the index is not an order fact',
|
|
580
|
+
rejects: noOrderEvidence,
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
/** The two tables together, so a caller ablates one rule by name without knowing which stage owns
|
|
585
|
+
* it. Defaulted on `inferGlobalArrays`; a test passes an ablated pair. */
|
|
586
|
+
export interface ArrayShapeGates {
|
|
587
|
+
readonly address: readonly Gate<AddressUse>[];
|
|
588
|
+
readonly shape: readonly Gate<ShapeEvidence>[];
|
|
589
|
+
}
|
|
590
|
+
export const ARRAY_SHAPE_GATES: ArrayShapeGates = { address: ADDRESS_GATES, shape: SHAPE_GATES };
|
|
591
|
+
|
|
592
|
+
/** The same pair for the ORDER half alone: the address rules that are about the address, and the
|
|
593
|
+
* shape rules that read `baseFirst`. Both halves are the shipped rule OBJECTS, so this table
|
|
594
|
+
* cannot drift from the one `inferGlobalArrays` asks. */
|
|
595
|
+
export const ORDER_LICENCE_GATES: ArrayShapeGates = { address: ELEMENT_ADDRESS_GATES, shape: ORDER_SHAPE_GATES };
|
|
596
|
+
|
|
597
|
+
/** Op → (block index, op index), for the order comparison. */
|
|
598
|
+
function positions(fn: Fn): Map<Op, { b: number; i: number }> {
|
|
599
|
+
const pos = new Map<Op, { b: number; i: number }>();
|
|
600
|
+
fn.blocks.forEach((blk, b) => blk.ops.forEach((op, i) => pos.set(op, { b, i })));
|
|
601
|
+
return pos;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Every op that reads `v` as an operand. Successor arguments count as uses too — a value handed
|
|
605
|
+
* across an edge leaves this function's address arithmetic, which `address-escapes` covers. */
|
|
606
|
+
function useIndex(fn: Fn): Map<Value, Op[]> {
|
|
607
|
+
const uses = new Map<Value, Op[]>();
|
|
608
|
+
const add = (v: Value, op: Op): void => {
|
|
609
|
+
uses.set(v, [...(uses.get(v) ?? []), op]);
|
|
610
|
+
};
|
|
611
|
+
for (const b of fn.blocks) {
|
|
612
|
+
for (const op of b.ops) {
|
|
613
|
+
op.operands.forEach((o) => add(o, op));
|
|
614
|
+
op.successors.forEach((s) => s.args.forEach((a) => add(a, op)));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return uses;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** `x * K` / `x << k` read as a scale, or scale 1 for anything else. A CONSTANT operand makes the
|
|
621
|
+
* whole term constant instead (`const << 2` is a displacement, not a subscript). */
|
|
622
|
+
function scaleOf(v: Value, defs: Map<Value, Op>): Term {
|
|
623
|
+
const d = defs.get(v);
|
|
624
|
+
const constOf = (x: Value): number | null => {
|
|
625
|
+
const dx = defs.get(x);
|
|
626
|
+
return dx?.opcode === 'const' ? (dx.attrs.value as number) : null;
|
|
627
|
+
};
|
|
628
|
+
if (d?.opcode === 'const') {
|
|
629
|
+
return { scale: 0, v: null, konst: d.attrs.value as number, scaleOp: null };
|
|
630
|
+
}
|
|
631
|
+
if (d?.opcode === 'shl' && d.operands.length === 1 && typeof d.attrs.imm === 'number') {
|
|
632
|
+
const k = d.attrs.imm;
|
|
633
|
+
const inner = constOf(d.operands[0]);
|
|
634
|
+
if (inner !== null) {
|
|
635
|
+
return { scale: 0, v: null, konst: inner << k, scaleOp: null };
|
|
636
|
+
}
|
|
637
|
+
return k > 0 && k < 31
|
|
638
|
+
? { scale: 1 << k, v: d.operands[0], konst: 0, scaleOp: d }
|
|
639
|
+
: { scale: 1, v, konst: 0, scaleOp: null };
|
|
640
|
+
}
|
|
641
|
+
if (d?.opcode === 'mul') {
|
|
642
|
+
for (const [a, b] of [
|
|
643
|
+
[d.operands[0], d.operands[1]],
|
|
644
|
+
[d.operands[1], d.operands[0]],
|
|
645
|
+
] as const) {
|
|
646
|
+
const k = constOf(b);
|
|
647
|
+
if (k !== null && k > 0 && constOf(a) === null) {
|
|
648
|
+
return { scale: k, v: a, konst: 0, scaleOp: d };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { scale: 1, v, konst: 0, scaleOp: null };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** The additive terms of a byte residual. Only `add` is opened: a `sub` at the top of the tree
|
|
656
|
+
* makes a term's sign depend on the walk, and a NEGATIVE stride is not an array subscript this
|
|
657
|
+
* spelling can express, so it refuses rather than dropping the sign. */
|
|
658
|
+
function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
|
|
659
|
+
const out: Term[] = [];
|
|
660
|
+
const walk = (v: Value, depth: number): boolean => {
|
|
661
|
+
if (depth > 16) {
|
|
662
|
+
return false; // a pathological address tree: refuse rather than walk it
|
|
663
|
+
}
|
|
664
|
+
const d = defs.get(v);
|
|
665
|
+
if (d?.opcode === 'sub') {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (d?.opcode === 'add') {
|
|
669
|
+
return walk(d.operands[0], depth + 1) && walk(d.operands[1], depth + 1);
|
|
670
|
+
}
|
|
671
|
+
out.push(scaleOf(v, defs));
|
|
672
|
+
return true;
|
|
673
|
+
};
|
|
674
|
+
return walk(root, 0) ? out : null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Every access of every named data global in `fn`, keyed by symbol — or, for a symbol any of
|
|
678
|
+
* whose uses `ADDRESS_GATES` rejects, the ID OF THE RULE that rejected it. The rejection is per
|
|
679
|
+
* SYMBOL and never withdrawn: what is being derived is a DECLARATION, and one use this spelling
|
|
680
|
+
* does not model makes the name keep the cast form everywhere. The id is carried rather than a
|
|
681
|
+
* bare null so a census asks which rule decided instead of re-deriving the predicates.
|
|
682
|
+
*
|
|
683
|
+
* An array-valued entry is NEVER EMPTY: `record` below is the only writer of the array form and
|
|
684
|
+
* every call appends, so a symbol reaches the map either with at least one access or as a refusal.
|
|
685
|
+
* Callers index `[0]` on that basis.
|
|
686
|
+
*
|
|
687
|
+
* `interiorIsEvidence` is the second consumer's half of the filter below: an access at a non-zero
|
|
688
|
+
* displacement says nothing about the ELEMENT (its width is the sub-word read's, not the
|
|
689
|
+
* element's) and everything about the ORDER (its terms and its `gaddr` are the same ones). The
|
|
690
|
+
* declaration derivation keeps the default and never sees one — which is what the two overloads
|
|
691
|
+
* say in the type. */
|
|
692
|
+
function accessesBySymbol(
|
|
693
|
+
fn: Fn,
|
|
694
|
+
gates: readonly Gate<AddressUse>[],
|
|
695
|
+
): Map<string, ElementAccess[] | { refusedBy: string }>;
|
|
696
|
+
function accessesBySymbol(
|
|
697
|
+
fn: Fn,
|
|
698
|
+
gates: readonly Gate<AddressUse>[],
|
|
699
|
+
interiorIsEvidence: true,
|
|
700
|
+
): Map<string, Access[] | { refusedBy: string }>;
|
|
701
|
+
function accessesBySymbol(
|
|
702
|
+
fn: Fn,
|
|
703
|
+
gates: readonly Gate<AddressUse>[],
|
|
704
|
+
interiorIsEvidence = false,
|
|
705
|
+
): Map<string, Access[] | { refusedBy: string }> {
|
|
706
|
+
const defs = defOpMap(fn);
|
|
707
|
+
const uses = useIndex(fn);
|
|
708
|
+
const out = new Map<string, Access[] | { refusedBy: string }>();
|
|
709
|
+
const refuse = (sym: string, id: string): void => void out.set(sym, { refusedBy: id });
|
|
710
|
+
const record = (sym: string, a: Access): void => {
|
|
711
|
+
const cur = out.get(sym);
|
|
712
|
+
if (cur !== undefined && !Array.isArray(cur)) {
|
|
713
|
+
return; // already refused: a refusal is per SYMBOL and never withdrawn
|
|
714
|
+
}
|
|
715
|
+
out.set(sym, [...(cur ?? []), a]);
|
|
716
|
+
};
|
|
717
|
+
for (const b of fn.blocks) {
|
|
718
|
+
for (const g of b.ops) {
|
|
719
|
+
if (g.opcode !== 'gaddr' || typeof g.attrs.sym !== 'string' || g.attrs.code === true) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const sym = g.attrs.sym;
|
|
723
|
+
const base = g.results[0];
|
|
724
|
+
const gUses = uses.get(base) ?? [];
|
|
725
|
+
if (gUses.length === 0) {
|
|
726
|
+
continue; // a dead address: no evidence either way, and nothing to spell
|
|
727
|
+
}
|
|
728
|
+
for (const u of gUses) {
|
|
729
|
+
const isAdd = u.opcode === 'add';
|
|
730
|
+
const other = isAdd ? (u.operands[0] === base ? u.operands[1] : u.operands[0]) : undefined;
|
|
731
|
+
const terms = other === undefined ? null : residualTerms(other, defs);
|
|
732
|
+
const consumers = (isAdd ? (uses.get(u.results[0]) ?? []) : []).map((m) => {
|
|
733
|
+
const isLoad = m.opcode === 'load' && m.operands[0] === u.results[0];
|
|
734
|
+
const isStore = m.opcode === 'store' && m.operands[0] === u.results[0];
|
|
735
|
+
return { m, isLoad, isStore, isElementAccess: (isLoad || isStore) && (m.attrs.off as number) === 0 };
|
|
736
|
+
});
|
|
737
|
+
const use: AddressUse = {
|
|
738
|
+
isAdd,
|
|
739
|
+
addendIsConst: other !== undefined && defs.get(other)?.opcode === 'const',
|
|
740
|
+
terms,
|
|
741
|
+
consumers,
|
|
742
|
+
};
|
|
743
|
+
const rejected = firstRejection(gates, use);
|
|
744
|
+
if (rejected !== null) {
|
|
745
|
+
refuse(sym, rejected);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
// WHICH ACCESSES ARE EVIDENCE, and it is a different set per consumer. The DECLARATION
|
|
749
|
+
// takes the whole-element ones only — filtered rather than assumed, because with
|
|
750
|
+
// `interior-or-non-access` ABLATED (a test does exactly that) an interior read would
|
|
751
|
+
// otherwise join the evidence and an ablation must remove a REFUSAL, never manufacture a
|
|
752
|
+
// fact. The ORDER consumer passes `interiorIsEvidence` and takes every load and store,
|
|
753
|
+
// because a read two bytes into the element says exactly as much about where the base was
|
|
754
|
+
// materialized as a whole-element one does — and says nothing about the element, which is
|
|
755
|
+
// why it is recorded with `elementWidth: null` rather than with the load's own width.
|
|
756
|
+
for (const c of consumers.filter((x) => (interiorIsEvidence ? x.isLoad || x.isStore : x.isElementAccess))) {
|
|
757
|
+
record(sym, {
|
|
758
|
+
elementWidth: c.isElementAccess ? (c.m.attrs.width as number) : null,
|
|
759
|
+
signed: c.isLoad && (c.m.attrs.signed as boolean) === true,
|
|
760
|
+
isLoad: c.isLoad,
|
|
761
|
+
terms: terms ?? [],
|
|
762
|
+
gaddr: g,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** The distinct STRIDES of an access's non-constant terms, ascending. EMPTY when the access has
|
|
772
|
+
* no non-constant term (a pure constant address — `&gSym + K` — which names no subscript);
|
|
773
|
+
* `no-subscript` is the gate that reads that. Deduped and sorted, which is the precondition
|
|
774
|
+
* `extentsOf` relies on. */
|
|
775
|
+
function stridesOf(a: Access): number[] {
|
|
776
|
+
return [...new Set(a.terms.filter((t) => t.v !== null).map((t) => t.scale))].sort((x, y) => x - y);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** THE ORDER LICENCE for one access, as a three-valued answer — and the third value is the point.
|
|
780
|
+
*
|
|
781
|
+
* `true` = every scaling of the index this walk CAN compare happens after the base was
|
|
782
|
+
* materialized (the array-subscript shape); `false` = at least one comparable scaling happens
|
|
783
|
+
* BEFORE it (the pointer shape, which is the cast spelling); `undefined` = there is nothing to
|
|
784
|
+
* compare, so the order says nothing at all and the symbol must find its licence elsewhere.
|
|
785
|
+
*
|
|
786
|
+
* ONLY A SCALING IN THE SAME BLOCK AS THE POOL LOAD IS COMPARABLE, and that is a statement about
|
|
787
|
+
* agbcc rather than a convenience. One `ldr` is SHARED by every access of the name and it is
|
|
788
|
+
* hoisted out of loops — both visible in the fixtures beside this rule — so once the load and a
|
|
789
|
+
* scaling sit in different blocks at least one of them has been MOVED relative to the expression
|
|
790
|
+
* that wrote it, and the function's instruction order no longer records `build_array_ref`'s
|
|
791
|
+
* expansion order. Compiled, that is exactly what happens — a loop whose
|
|
792
|
+
* only subscript is in the body hoists the `ldr` into the preheader and the two spellings
|
|
793
|
+
*
|
|
794
|
+
* for (i = 0; i < n; i++) s += gTbl[p[i]];
|
|
795
|
+
* for (i = 0; i < n; i++) s += ((u16 *)gTbl)[p[i]];
|
|
796
|
+
*
|
|
797
|
+
* become ONE object, byte-identical `.s` included. So a cross-block scaling gets `undefined`
|
|
798
|
+
* rather than `false`: answering `false` there would be the positive claim "the index was scaled
|
|
799
|
+
* first" about an access that makes no claim either way, and the symbol belongs to
|
|
800
|
+
* `no-positive-evidence`, which is what "this says nothing" is called in that table — a refusal
|
|
801
|
+
* either way, but the true one.
|
|
802
|
+
*
|
|
803
|
+
* The discrimination the rule exists for is untouched, because it lives in the SAME-BLOCK
|
|
804
|
+
* accesses: compiled, a function that subscripts once outside a loop and once inside it is a
|
|
805
|
+
* different object under the two spellings, and the difference is at the access outside. */
|
|
806
|
+
function baseFirst(a: Access, pos: Map<Op, { b: number; i: number }>): boolean | undefined {
|
|
807
|
+
const g = pos.get(a.gaddr);
|
|
808
|
+
if (g === undefined) {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
const comparable = a.terms
|
|
812
|
+
.map((t) => (t.scaleOp === null ? undefined : pos.get(t.scaleOp)))
|
|
813
|
+
.filter((p): p is { b: number; i: number } => p !== undefined && p.b === g.b);
|
|
814
|
+
return comparable.length === 0 ? undefined : comparable.every((p) => p.i > g.i);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The shape one symbol's accesses evidence, or null where `SHAPE_GATES` rejects. Decided over
|
|
818
|
+
* ALL of the symbol's accesses at once, because what is being derived is a DECLARATION: one
|
|
819
|
+
* element type and one rank for the name, or none.
|
|
820
|
+
*
|
|
821
|
+
* The ELEMENT SIGNEDNESS this returns is a PICK, not a reading — see the module note's compiled
|
|
822
|
+
* pair. Where the source ends up resting on it, it travels out as an assumption
|
|
823
|
+
* (`assumedShapes` → `DecompileResult.assumedSymbols`) rather than being applied silently. */
|
|
824
|
+
function evidenceOf(accs: Access[], pos: Map<Op, { b: number; i: number }>): ShapeEvidence {
|
|
825
|
+
const perAccess = accs.map((a) => {
|
|
826
|
+
const strides = stridesOf(a);
|
|
827
|
+
// THIS access's own width, never the first one's. `accs[0]` is safe only behind
|
|
828
|
+
// `mixed-access-width`, which is in the declaration table and NOT in the order table — so on
|
|
829
|
+
// the second consumer a clean access recorded ahead of an interior one would lend the interior
|
|
830
|
+
// one its width, and a mid-element test against a fabricated element is a lie in the licence's
|
|
831
|
+
// favour. Null — an interior access, which only the order consumer records — is no element
|
|
832
|
+
// width, so there is no whole number of elements for a constant to be, and the answer is the
|
|
833
|
+
// refusal.
|
|
834
|
+
const width = a.elementWidth;
|
|
835
|
+
return {
|
|
836
|
+
elementWidth: width,
|
|
837
|
+
strides,
|
|
838
|
+
extents: extentsOf(strides),
|
|
839
|
+
midElementConst: width === null || a.terms.some((t) => t.v === null && t.konst % width !== 0),
|
|
840
|
+
baseFirst: baseFirst(a, pos),
|
|
841
|
+
};
|
|
842
|
+
});
|
|
843
|
+
const ws = new Set(accs.map((a) => a.elementWidth));
|
|
844
|
+
return {
|
|
845
|
+
widths: {
|
|
846
|
+
mixed: ws.size !== 1,
|
|
847
|
+
hasInterior: ws.has(null),
|
|
848
|
+
anySubWord: [...ws].some((w) => w !== null && w < 4),
|
|
849
|
+
},
|
|
850
|
+
signs: [...new Set(accs.map((a) => a.signed))],
|
|
851
|
+
perAccess,
|
|
852
|
+
constOnIndex: accs.some((a) => a.terms.some((t) => t.v === null && t.konst !== 0)),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function shapeOf(
|
|
857
|
+
accs: ElementAccess[],
|
|
858
|
+
pos: Map<Op, { b: number; i: number }>,
|
|
859
|
+
gates: readonly Gate<ShapeEvidence>[],
|
|
860
|
+
): SymbolInfo | null {
|
|
861
|
+
const ev = evidenceOf(accs, pos);
|
|
862
|
+
if (firstRejection(gates, ev) !== null) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
// The DECLARED signedness is the loads' — a store extends nothing, and `mixed-extension` has
|
|
866
|
+
// already refused a name whose loads disagree at a width where it shows.
|
|
867
|
+
const loadSigns = new Set(accs.filter((a) => a.isLoad).map((a) => a.signed));
|
|
868
|
+
const dims = ev.perAccess[0].extents ?? [];
|
|
869
|
+
return {
|
|
870
|
+
name: (accs[0].gaddr.attrs.sym as string) ?? '',
|
|
871
|
+
kind: 'data',
|
|
872
|
+
shape: 'array',
|
|
873
|
+
elemSize: accs[0].elementWidth,
|
|
874
|
+
elemSigned: loadSigns.size === 1 ? [...loadSigns][0] : false,
|
|
875
|
+
...(dims.length ? { dims: [null, ...dims] } : {}),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** The INNER extents of a declared rank, read out of ascending strides: each stride must be a
|
|
880
|
+
* whole multiple — at least 2× — of the one below it, or two positions in the array would be
|
|
881
|
+
* indistinguishable and the split would be a guess. Rank 1 is `[]`. */
|
|
882
|
+
function extentsOf(strides: number[]): number[] | null {
|
|
883
|
+
const extents: number[] = [];
|
|
884
|
+
for (let i = 1; i < strides.length; i++) {
|
|
885
|
+
const k = strides[i] / strides[i - 1];
|
|
886
|
+
// NOT AN INTEGER is the live half; `k < 2` is belt-and-braces on a precondition `stridesOf`
|
|
887
|
+
// already establishes (it dedups through a Set and sorts, so consecutive strides are strictly
|
|
888
|
+
// ascending and an integer ratio is at least 2). Kept as an assertion of what this function
|
|
889
|
+
// needs from its caller, not as a rule with an inhabitant — an extent of 1 would make two
|
|
890
|
+
// positions in the array name one cell.
|
|
891
|
+
if (!Number.isInteger(k) || k < 2) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
extents.unshift(k);
|
|
895
|
+
}
|
|
896
|
+
return extents;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** Do two `SymbolInfo`s say the same thing about the ONE question the bare subscript asks — which
|
|
900
|
+
* object the name denotes, cell by cell? Every field `bareArrayElement` / `bareArrayLead` /
|
|
901
|
+
* `declaredSubscripts` (structure/globalaccess.ts) and the array branch of the declaration
|
|
902
|
+
* renderer (declare.ts) read, and nothing else: two entries agreeing here spell and declare the
|
|
903
|
+
* same addresses whichever one a consumer picked up.
|
|
904
|
+
*
|
|
905
|
+
* This exists because the SPELLING and the DECLARATION are decided from different derivations —
|
|
906
|
+
* rank.ts derives the declaration dictionary once off the probe's lift and the spelling per
|
|
907
|
+
* symbol variant off that variant's own — and a candidate that spells from one and declares from
|
|
908
|
+
* the other addresses a different object than the assembly did, compiling either way. */
|
|
909
|
+
export function sameDerivedShape(a: SymbolInfo | undefined, b: SymbolInfo | undefined): boolean {
|
|
910
|
+
if (a === undefined || b === undefined) {
|
|
911
|
+
return a === b;
|
|
912
|
+
}
|
|
913
|
+
const dims = (i: SymbolInfo): string => JSON.stringify(i.shape === 'array' ? (i.dims ?? null) : null);
|
|
914
|
+
return (
|
|
915
|
+
a.shape === b.shape &&
|
|
916
|
+
a.elemSize === b.elemSize &&
|
|
917
|
+
(a.elemSigned ?? false) === (b.elemSigned ?? false) &&
|
|
918
|
+
dims(a) === dims(b)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** The array shapes `fn`'s own assembly evidences for the globals it names, keyed by name.
|
|
923
|
+
*
|
|
924
|
+
* Runs on the LIFTED function (see the module note: the raising tower destroys the order the
|
|
925
|
+
* licence reads). Empty for every target that has not opted in, and empty where nothing is
|
|
926
|
+
* evidenced — never a partial guess. */
|
|
927
|
+
export function inferGlobalArrays(
|
|
928
|
+
fn: Fn,
|
|
929
|
+
target: TargetDescription,
|
|
930
|
+
gates: ArrayShapeGates = ARRAY_SHAPE_GATES,
|
|
931
|
+
): Map<string, SymbolInfo> {
|
|
932
|
+
const out = new Map<string, SymbolInfo>();
|
|
933
|
+
if (target.compilerBehaviors.arrayShapeFromStride !== true) {
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
const pos = positions(fn);
|
|
937
|
+
for (const [sym, accs] of accessesBySymbol(fn, gates.address)) {
|
|
938
|
+
const si = Array.isArray(accs) ? shapeOf(accs, pos, gates.shape) : null;
|
|
939
|
+
if (si !== null) {
|
|
940
|
+
out.set(sym, si);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** The globals whose address `fn`'s assembly materialized BEFORE it scaled the index — the ORDER
|
|
947
|
+
* half of the licence, on its own (see THE ORDER LICENCE HAS TWO CONSUMERS in the header).
|
|
948
|
+
*
|
|
949
|
+
* A SUPERSET of the names `inferGlobalArrays` shapes: a name that one shapes has no interior
|
|
950
|
+
* consumer at all (`interior-or-non-access` refused every symbol that does), so both derivations
|
|
951
|
+
* see the identical accesses and this one asks strictly fewer rules of them. The difference is the
|
|
952
|
+
* point — a name read at a DISPLACEMENT, a struct element among them, is licensed here and refused
|
|
953
|
+
* there. What this set is NOT is a declaration: it travels through no honesty channel, because a
|
|
954
|
+
* pointer local over `&gSym` reproduces the bytes under any declaration of `gSym`, exactly as the
|
|
955
|
+
* cast spelling it re-homes does.
|
|
956
|
+
*
|
|
957
|
+
* A GENERATOR, never a classifier. Its consumer (`l3/basecse.ts`'s `order-licensed`, reached from
|
|
958
|
+
* rank's `/orderbase` roster row) uses it to OFFER a candidate beside the inline spelling, which
|
|
959
|
+
* the differ then referees — so an over-licensed name costs fan and a tie-break, never meaning.
|
|
960
|
+
* Empty for every target that has not opted in. */
|
|
961
|
+
export function orderLicensedGlobals(
|
|
962
|
+
fn: Fn,
|
|
963
|
+
target: TargetDescription,
|
|
964
|
+
gates: ArrayShapeGates = ORDER_LICENCE_GATES,
|
|
965
|
+
): ReadonlySet<string> {
|
|
966
|
+
const out = new Set<string>();
|
|
967
|
+
if (target.compilerBehaviors.arrayShapeFromStride !== true) {
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
const pos = positions(fn);
|
|
971
|
+
for (const [sym, accs] of accessesBySymbol(fn, gates.address, true)) {
|
|
972
|
+
if (Array.isArray(accs) && firstRejection(gates.shape, evidenceOf(accs, pos)) === null) {
|
|
973
|
+
out.add(sym);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Which rule refused each name this function's pool spells, or null where a shape was derived —
|
|
980
|
+
* the attribution `firstRejection` exists for. NOT on the shipped path: a caller instrumenting a
|
|
981
|
+
* refusal (a census, a plan that needs the FIRST guard rather than the co-occurring set) asks
|
|
982
|
+
* here instead of re-deriving the predicates — the one place an attribution is measured rather
|
|
983
|
+
* than asserted. */
|
|
984
|
+
export function arrayShapeRefusals(
|
|
985
|
+
fn: Fn,
|
|
986
|
+
target: TargetDescription,
|
|
987
|
+
gates: ArrayShapeGates = ARRAY_SHAPE_GATES,
|
|
988
|
+
): Map<string, string | null> {
|
|
989
|
+
const out = new Map<string, string | null>();
|
|
990
|
+
if (target.compilerBehaviors.arrayShapeFromStride !== true) {
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
const pos = positions(fn);
|
|
994
|
+
for (const [sym, accs] of accessesBySymbol(fn, gates.address)) {
|
|
995
|
+
out.set(sym, Array.isArray(accs) ? firstRejection(gates.shape, evidenceOf(accs, pos)) : accs.refusedBy);
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** THE SHAPES THE EMITTED SOURCE ACTUALLY RESTS ON — which is a strictly smaller set than the
|
|
1001
|
+
* shapes this module DERIVED, and the difference is the whole reason the two are separate
|
|
1002
|
+
* functions rather than one.
|
|
1003
|
+
*
|
|
1004
|
+
* `inferGlobalArrays` answers "what does this assembly evidence". The honesty channel
|
|
1005
|
+
* (`DecompileResult.assumedSymbols`, `TraceReport.assumedSymbols`, the cli's `[assumed]` block,
|
|
1006
|
+
* the playground's panel) answers a different question: which declarations is the reader obliged
|
|
1007
|
+
* to check, because the source in front of them is right only beside those declarations. A
|
|
1008
|
+
* derived shape earns that obligation only where BOTH of these hold, and a shape can fail either
|
|
1009
|
+
* one:
|
|
1010
|
+
*
|
|
1011
|
+
* 1. THE STRUCTURER ACTUALLY SPELLED THE NAME BARE. A derivation reaching a symbol does not
|
|
1012
|
+
* make the source depend on it — every consumer of a shape can still refuse, and then the
|
|
1013
|
+
* access keeps `((T *)&gSym)[i]`, which reproduces the bytes under ANY declaration and
|
|
1014
|
+
* therefore assumes nothing. `kleod:SetupBG3WindowOverlay` is that case on the corpus: the
|
|
1015
|
+
* `gBgInfo` shape derives (element 4) and `arrayAccess` declines it (the access carries a
|
|
1016
|
+
* field offset), so the emitted source casts and the reader has nothing to check.
|
|
1017
|
+
* 2. NO SYMBOL MAP DESCRIBED THE NAME. `structure()` asks the project's map FIRST, so on a
|
|
1018
|
+
* map-ful function the spelling is the MAP's and the derived shape never reached the source
|
|
1019
|
+
* — including when the map CONTRADICTS it, which is the sharp case (`sa3:sa2__sub_8083504`
|
|
1020
|
+
* derives `elemSigned: false` off its own `ldrh` while the vendored map declares
|
|
1021
|
+
* `const s16 gSineTable[1280]`). Publishing the derivation there tells the reader to check a
|
|
1022
|
+
* declaration against headers that already answered, and answered differently. Where the map
|
|
1023
|
+
* AGREES the name may well be spelled bare — but then it is the MAP's declaration the source
|
|
1024
|
+
* rests on, supplied by the caller, and nothing was assumed.
|
|
1025
|
+
*
|
|
1026
|
+
* `SFn.globals` IS test (1): `structure()` populates it through `noteGlobal`, which is called at
|
|
1027
|
+
* exactly the three declaration-dependent bare-array spellings (structure.ts — `bareArrayLead` on
|
|
1028
|
+
* each of the byte-address and element-index paths, and `declaredSubscripts`) and nowhere else.
|
|
1029
|
+
* A fourth caller would have to be added to this list too, which is why the coupling is stated
|
|
1030
|
+
* here rather than left to be rediscovered. */
|
|
1031
|
+
export function assumedShapes(
|
|
1032
|
+
inferred: Map<string, SymbolInfo>,
|
|
1033
|
+
sfn: SFn,
|
|
1034
|
+
mapSymbols?: { has(name: string): boolean },
|
|
1035
|
+
): SymbolInfo[] {
|
|
1036
|
+
const spelledBare = new Set((sfn.globals ?? []).map((g) => g.name));
|
|
1037
|
+
return [...inferred.values()].filter((i) => spelledBare.has(i.name) && mapSymbols?.has(i.name) !== true);
|
|
1038
|
+
}
|