@asmlift/core 0.8.1 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// asmlift — A NARROW READ OF A WIDER FIELD, which the struct recognizers see as a second field at
|
|
2
|
+
// the same offset.
|
|
3
|
+
//
|
|
4
|
+
// WHAT IS MISSING WITHOUT THIS. A compiler asked for the low byte of a halfword member emits a byte
|
|
5
|
+
// load at the halfword's own address:
|
|
6
|
+
//
|
|
7
|
+
// BlendPalette(..., task->data[6], ...) // data[] is s16, the parameter is u8
|
|
8
|
+
// ldrh r0, [r4, #0xc] @ the member, read whole
|
|
9
|
+
// ldrb r2, [r4, #0xc] @ the same member, narrowed for the call
|
|
10
|
+
//
|
|
11
|
+
// The pass that groups a base's constant-offset accesses reads that as ONE OFFSET AT TWO WIDTHS:
|
|
12
|
+
// `raise/structs.ts` declares the pair a union member (on a parameter or a loaded pointer). That
|
|
13
|
+
// reading is right about the SHAPE and wrong about the CAUSE — there is no second object here, only
|
|
14
|
+
// a cast — so the function would carry a union its source never declared. (`raise/memberarrays.ts`'s
|
|
15
|
+
// same-sounding `member-conflict` judges VARIABLE-INDEX accesses, `aload`/`astore`, and a base with
|
|
16
|
+
// any constant-offset load is already outside that pass by `direct-access`. It is a different shape
|
|
17
|
+
// and this fold does not reach it.)
|
|
18
|
+
//
|
|
19
|
+
// THE ASM DOES NOT SAY WHICH IT IS, and that is the point: a union, a cast, a bitfield and two
|
|
20
|
+
// adjacent bytes the compiler fused all produce the same two instructions. What this pass claims is
|
|
21
|
+
// narrower than any of those readings. `(u8)x` and a byte load of `x`'s low-order byte denote THE
|
|
22
|
+
// SAME BYTES AND THE SAME VALUE, so rewriting one into the other invents no type and loses no
|
|
23
|
+
// fidelity; what the struct recognizers then do with a base carrying one width instead of two is
|
|
24
|
+
// their existing job, refereed by the differ as before.
|
|
25
|
+
//
|
|
26
|
+
// MEASURED, not argued, on the row that motivated it
|
|
27
|
+
// (`pokeemerald:AnimTask_FlashHealthboxOnLevelUp_Step:agbcc`): the reference source declares ONE
|
|
28
|
+
// `s16` member at offset 12 and passes it to a `u8` parameter, and agbcc compiles that to the
|
|
29
|
+
// `ldrh`/`ldrb` pair above — `.text` sha256 `e0524eb341119a21`, 204 bytes, byte-identical to the
|
|
30
|
+
// target object. So the single-field layout this pass makes reachable is the layout the ROM was
|
|
31
|
+
// built from.
|
|
32
|
+
//
|
|
33
|
+
// WHAT IT DOES. Over the L1 fn, for every base with constant-offset accesses: a LOAD whose byte
|
|
34
|
+
// range is the LOW-ORDER END of a strictly wider access's range at the same base becomes a
|
|
35
|
+
// `zext`/`sext` — matching the narrow load's own signedness — of a fresh load at the wider range.
|
|
36
|
+
// No opcode, no representation, no level: `zext`/`sext` with a `width` attr already exist
|
|
37
|
+
// (`ir/opcodes.ts`), they already print as a C cast, and the backend already recompiles that cast
|
|
38
|
+
// to the narrow instruction on the compilers that emit it.
|
|
39
|
+
//
|
|
40
|
+
// THE FOLD WIDENS A READ, which is the one thing worth arguing about, and the two rules that bound
|
|
41
|
+
// it are `fixed-cell` and `covering-dominates` below. Neither is a restatement of what
|
|
42
|
+
// raise/structs.ts already claims, and the difference is what makes them rules rather than
|
|
43
|
+
// decoration: that pass declares a SINGLE LAYOUT covering every offset the function touches, which
|
|
44
|
+
// is a claim the compiler reads and no instruction executes, while this one emits a DEREFERENCE.
|
|
45
|
+
// Widening a read where the covering access is on the other arm of a branch would have the emitted
|
|
46
|
+
// program load bytes that no execution reaching that point ever loaded — a compile-time claim
|
|
47
|
+
// escalated into a runtime one. Widening a read of a cell at a constant address would change the
|
|
48
|
+
// event a device sees. So the range this fold widens to must be a range some access at the same
|
|
49
|
+
// base read ON THIS PATH, at a cell whose declaration cannot make its access width observable.
|
|
50
|
+
//
|
|
51
|
+
// LOW-ORDER END IS AN ENDIANNESS QUESTION, read off `capabilities.endianness` rather than assumed:
|
|
52
|
+
// the low bytes of a big-endian field sit at its TOP address (`lw 20(a0)` covering `lhu 22(a0)`),
|
|
53
|
+
// the low bytes of a little-endian one at its base. Both directions have a corpus inhabitant and
|
|
54
|
+
// the benchmark's `unitrunc` row carries both.
|
|
55
|
+
//
|
|
56
|
+
// WHICH GATE BOUNDS THE CORPUS PATH — `pnpm bench gates --pass truncload [--toolchain <id>]`, and
|
|
57
|
+
// the answer is the SOUND ones, per population. Over the synthetic tier on agbcc (337 rows) the
|
|
58
|
+
// census reads `covering-store 12, covering-dominates 4, fixed-cell 2`; on ido7.1 (179 rows) and
|
|
59
|
+
// gcc2.7.2kmc (183) `covering-store 12, high-order-read 4, fixed-cell 2`, the big-endian rows
|
|
60
|
+
// reaching the skew rule because `utag`'s union members are read at the field's TOP; on
|
|
61
|
+
// mwcc_242_81 (189) `covering-store 12, fixed-cell 2`. The union rows (`uhalf`, `ureread`, `utag`)
|
|
62
|
+
// are most of it, and they are counted whole only because raise/structs.ts
|
|
63
|
+
// recovers them rather than declining part-way through the enumeration — the next paragraph.
|
|
64
|
+
//
|
|
65
|
+
// READ THE `did not lift` LINE WITH THE COUNTS, because the census runs the whole RANKED
|
|
66
|
+
// enumeration per row and a row that THROWS part-way through it contributes a TRUNCATED count
|
|
67
|
+
// rather than none. `synthetic:unidev` is the worked example: while `raise/structs.ts` declined it
|
|
68
|
+
// the agbcc census read `fixed-cell 1` with 13 rows unlifted, and once that pass stopped declining
|
|
69
|
+
// a literal-address base it read `fixed-cell 2` with 12 — the same refusals, all of them now
|
|
70
|
+
// reached. `--only synthetic:unidev:agbcc` shows the whole delta on that one row.
|
|
71
|
+
//
|
|
72
|
+
// `high-order-read` is STARVED on agbcc — not because nothing
|
|
73
|
+
// skews there, but because `synthetic:uhalf`'s skewed read sits on a base whose widest access is
|
|
74
|
+
// the `u->w = v` STORE, so the earlier rule answers first. Real rows show two of them alone:
|
|
75
|
+
// `--only sa3:sub_804DC38:agbcc` reads `high-order-read 4`, an `ldrb` at byte 5 of a word at 4,
|
|
76
|
+
// and `--only kleod:WorldMapScreenUnlockNewWorld:agbcc` reads `fixed-cell 90` — a named global's
|
|
77
|
+
// cells, which is the same shape a named hardware register has. Two rules are UNINHABITED and say
|
|
78
|
+
// so rather than being left to look load-bearing: `unresolved-base` and `cast-width` are refusals
|
|
79
|
+
// no corpus row reaches, and both are pinned by a hand-written shape in the unit tests instead.
|
|
80
|
+
//
|
|
81
|
+
// A THIRD SHAPE IS ADMITTED AND SAYS SO RATHER THAN BEING CLAIMED SOUND: a cell reached at a
|
|
82
|
+
// RUNTIME index from a literal or a named base — `((vu16 *)0x4000000)[i]`, the GBA palette and DMA
|
|
83
|
+
// loops. `fixed-cell` does not fire there, because no constant address exists to read, and nothing
|
|
84
|
+
// else can: the IR for that loop and the IR for an element of an ordinary global array are the
|
|
85
|
+
// SAME IR, so refusing it would refuse every indexed access into a named object — including the
|
|
86
|
+
// map-less spelling of the row this pass was written for. Where the access is qualified — the lift
|
|
87
|
+
// pinned it as a device register's, or the map declares its object volatile — `qualified-access`
|
|
88
|
+
// refuses it; any other one is a residue, it is unpriced, and the place it would be settled is the
|
|
89
|
+
// symbol map, not this pass.
|
|
90
|
+
//
|
|
91
|
+
// A NARROW STORE IS NOT A CANDIDATE AT ALL — the refusal is in the candidate builder below rather
|
|
92
|
+
// than in the gate table, and this is the table's named residue. Widening a write clobbers the
|
|
93
|
+
// bytes past it, and no cast spelling expresses a partial write: `(u8)p->f = v` is not C. A base
|
|
94
|
+
// whose conflict is a narrow STORE therefore keeps both widths — two views of a union member where
|
|
95
|
+
// raise/structs.ts synthesizes the layout, two casts at a declared address.
|
|
96
|
+
//
|
|
97
|
+
// THE SECOND RESIDUE. A union read through members of three widths on three arms of a `switch`
|
|
98
|
+
// (`synthetic:utag`) is refused by `covering-dominates`, and the overlap it leaves is declared as a
|
|
99
|
+
// union member by raise/structs.ts — which is what that source wrote. Widening its reads DOES
|
|
100
|
+
// compile byte-exactly on agbcc, which is exactly why the rule has to be a rule: the differ scores
|
|
101
|
+
// the sound and the unsound spelling the same, so nothing downstream would ever refuse it.
|
|
102
|
+
import { constAddressOf, globalCellOf } from '../ir/alias';
|
|
103
|
+
import { type Block, type Fn, type Op, type Value, defOpMap, dominators, mkOp, mkValue } from '../ir/core';
|
|
104
|
+
import { qualified } from '../ir/discipline';
|
|
105
|
+
import { CAST_WIDTHS } from '../ir/opcodes';
|
|
106
|
+
import { T } from '../ir/types';
|
|
107
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
108
|
+
import { type Access, constOffsetAccesses } from './structs';
|
|
109
|
+
|
|
110
|
+
/** Ops through which a base address is still the SAME address question — the arithmetic a lift
|
|
111
|
+
* produces between a root and a load's operand. Anything else (a `load`, a `call`, a `laddr`) is
|
|
112
|
+
* a value the function obtained, which ENDS the walk: a pointer it was handed is an object the
|
|
113
|
+
* source declared, and there is nothing further to resolve. */
|
|
114
|
+
const ADDRESS_ARITHMETIC: ReadonlySet<string> = new Set(['add', 'sub', 'or', 'and', 'xor', 'shl', 'shr_u', 'shr_s']);
|
|
115
|
+
|
|
116
|
+
/** Does this base's origin RESOLVE — does the walk above reach a root it can name on every branch?
|
|
117
|
+
* An entry parameter, a literal, a named global and a loaded pointer all resolve; a block param
|
|
118
|
+
* joining two edges and an `undef` do not.
|
|
119
|
+
*
|
|
120
|
+
* It answers ONE question and `fixed-cell` answers the other: whether the cell is at a constant
|
|
121
|
+
* address is asked of the resolved base, and a base that never resolved cannot be asked at all.
|
|
122
|
+
* WHY THE WALK IS HERE rather than composed out of ir/alias.ts's `constAddressOf` /
|
|
123
|
+
* `globalBaseOf`: both of those return null for "not one of these" AND for "cannot tell", so a
|
|
124
|
+
* caller composing them cannot separate a parameter from a join — and the join is the case that
|
|
125
|
+
* has to refuse. */
|
|
126
|
+
function baseResolves(defs: Map<Value, Op>, entryParams: ReadonlySet<Value>, v: Value): boolean {
|
|
127
|
+
const d = defs.get(v);
|
|
128
|
+
if (!d) {
|
|
129
|
+
return entryParams.has(v);
|
|
130
|
+
}
|
|
131
|
+
if (d.opcode === 'undef') {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return !ADDRESS_ARITHMETIC.has(d.opcode) || d.operands.every((o) => baseResolves(defs, entryParams, o));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What the gates below judge: one narrow LOAD and the access at its base that covers it. */
|
|
138
|
+
export interface TruncatedLoad {
|
|
139
|
+
/** the narrow load reads a cell at a COMPILE-TIME CONSTANT address */
|
|
140
|
+
fixedCell: boolean;
|
|
141
|
+
/** the narrow load or its cover is a qualified access: pinned by the lift, or of an object the
|
|
142
|
+
* map declares volatile */
|
|
143
|
+
qualified: boolean;
|
|
144
|
+
/** the base's origin resolves — it is not a join the walk could not finish */
|
|
145
|
+
resolvedBase: boolean;
|
|
146
|
+
/** the covering access is a LOAD, so its signedness is read rather than invented */
|
|
147
|
+
coveringLoad: boolean;
|
|
148
|
+
/** the narrow bytes are the low-order end of the covering range on THIS target's endianness */
|
|
149
|
+
lowOrderEnd: boolean;
|
|
150
|
+
/** the covering access runs, on EVERY path that reaches the narrow load, before it */
|
|
151
|
+
coveringDominates: boolean;
|
|
152
|
+
/** the width in BITS the cast would carry */
|
|
153
|
+
castWidth: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const TRUNC_LOAD_GATES: readonly Gate<TruncatedLoad>[] = [
|
|
157
|
+
{
|
|
158
|
+
// SOUND, and it is the ACCESS WIDTH that is at stake rather than the value: a hardware register
|
|
159
|
+
// answers a byte read and a halfword read differently, so reading two bytes where the machine
|
|
160
|
+
// read one is an event the device sees. A memory-mapped register is a FIXED cell, and both
|
|
161
|
+
// spellings a source has for one land here: the bare cast of a number (`*(vu16 *)0x4000004`)
|
|
162
|
+
// through `constAddressOf`, and the named register (`REG_DISPSTAT`) through `globalCellOf`,
|
|
163
|
+
// whose `volatile` is a declaration in the project's headers that no raise pass can read.
|
|
164
|
+
// The two helpers are ir/alias.ts's, and `constAddressOf` is the address structure.ts's
|
|
165
|
+
// `volatileQualifiable` asks about — so a load this rule and `qualified-access` admit is one that
|
|
166
|
+
// predicate answers no to, by construction rather than by measurement.
|
|
167
|
+
//
|
|
168
|
+
// NOT `capabilities.deviceRegisters`. That window is documented as a question about SPELLING
|
|
169
|
+
// and may be approximate (target.ts), so a soundness rule may not rest on it; asking whether
|
|
170
|
+
// the address is constant at all is wider and needs nothing approximate.
|
|
171
|
+
id: 'fixed-cell',
|
|
172
|
+
why: 'a cell at a constant address can be a hardware register, where the access WIDTH is itself observable',
|
|
173
|
+
sound: true,
|
|
174
|
+
guardedBy: 'truncload.test.ts: a cell at a constant address is not narrowed',
|
|
175
|
+
rejects: (c) => c.fixedCell,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
// SOUND, and the residue of the rule above rather than a rule of its own: a base that joins two
|
|
179
|
+
// edges has no definition to walk, so `fixed-cell` cannot ask its question and the honest
|
|
180
|
+
// answer is that this pass does not know what the base is. An `undef` base is the same shape —
|
|
181
|
+
// storage nothing was entitled to write.
|
|
182
|
+
id: 'unresolved-base',
|
|
183
|
+
why: 'a base whose origin does not resolve cannot be shown not to be a device cell',
|
|
184
|
+
sound: true,
|
|
185
|
+
guardedBy: 'truncload.test.ts: a base joined on two edges is not narrowed',
|
|
186
|
+
rejects: (c) => !c.resolvedBase,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
// SOUND, and `fixed-cell`'s question answered by the passes that knew it: the device pin
|
|
190
|
+
// (frontend/device-pins.ts) marks a register's access whatever its address is, including the
|
|
191
|
+
// runtime-indexed channel `(vu32 *)(0x40000B0 + ch * 12)` no constant address reaches, and the
|
|
192
|
+
// `declared` stamp (raise/declared-volatile.ts) marks a read of a volatile object through a
|
|
193
|
+
// runtime index into it. Widened, the narrow access becomes a cast of a fresh load that carries
|
|
194
|
+
// no placement, at a width the machine never used on the object.
|
|
195
|
+
id: 'qualified-access',
|
|
196
|
+
why: 'a qualified access is made at its own width, and the fold would also drop its placement',
|
|
197
|
+
sound: true,
|
|
198
|
+
guardedBy: 'truncload.test.ts: a pinned narrow read through a runtime-indexed base keeps its width and its pin',
|
|
199
|
+
rejects: (c) => c.qualified,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
// The sibling narrowing passes' rule, under the sibling name (raise/paramwidth.ts,
|
|
203
|
+
// raise/narrowlocal.ts): a width `CAST_WIDTHS` does not carry has no C cast to spell it, so the
|
|
204
|
+
// fold would mint a `zext` the backend can only gap on. No corpus row reaches it today — every
|
|
205
|
+
// frontend load is 1, 2 or 4 bytes — and it costs a line rather than an argument about which
|
|
206
|
+
// widths a frontend may acquire.
|
|
207
|
+
id: 'cast-width',
|
|
208
|
+
why: 'only 8 and 16 are widths a `zext`/`sext` — and so a C declaration — carries',
|
|
209
|
+
sound: true,
|
|
210
|
+
guardedBy: 'truncload.test.ts: a width no C type spells is refused',
|
|
211
|
+
rejects: (c) => !CAST_WIDTHS.has(c.castWidth),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
// A covering STORE fixes the bytes but not the field's signedness, and the fold has to put one
|
|
215
|
+
// on the load it synthesizes. Not sound — the bytes are the same either way, so the worst case
|
|
216
|
+
// is a field declared `u16` where the source wrote `s16` — but inventing a fact the asm did not
|
|
217
|
+
// carry is not how a default spelling is chosen, and the differ never sees this one because the
|
|
218
|
+
// synthesized load is not enumerated.
|
|
219
|
+
id: 'covering-store',
|
|
220
|
+
why: 'a covering store carries no signedness, so the field type would be invented rather than read',
|
|
221
|
+
sound: false,
|
|
222
|
+
rejects: (c) => !c.coveringLoad,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
// SOUND: a read of bytes ABOVE the field's low-order end is `(u16)(x >> 16)`, not `(u16)x`.
|
|
226
|
+
// The cast spelling would read the field's other half — a different value from the one the
|
|
227
|
+
// machine loaded, silently. The shifted form is expressible, but it is not what the narrow
|
|
228
|
+
// instruction spells and nothing would recompile to it, so the honest answer is to leave the
|
|
229
|
+
// base declining.
|
|
230
|
+
id: 'high-order-read',
|
|
231
|
+
why: 'bytes above the low-order end of a field are a shift, and a plain cast would read the wrong ones',
|
|
232
|
+
sound: true,
|
|
233
|
+
guardedBy: 'truncload.test.ts: a read above the low-order end is not narrowed',
|
|
234
|
+
rejects: (c) => !c.lowOrderEnd,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
// SOUND, and the rule that separates this fold from the layout claim it feeds. Widening the
|
|
238
|
+
// read puts a WIDER ACCESS at the point where the machine executed a narrower one, so the bytes
|
|
239
|
+
// past the narrow range have to be bytes this execution already read — not merely bytes the
|
|
240
|
+
// FUNCTION reads somewhere. raise/structs.ts declares one layout per base across the whole
|
|
241
|
+
// function, which is a claim about the object's TYPE and costs no instruction; this fold emits
|
|
242
|
+
// a dereference, so the same function-wide reasoning would have the program read three bytes it
|
|
243
|
+
// never read on the path it is on. Dominance is what makes the two claims different sizes.
|
|
244
|
+
id: 'covering-dominates',
|
|
245
|
+
why: 'the bytes past the narrow read must be bytes this execution already read, not bytes some other path reads',
|
|
246
|
+
sound: true,
|
|
247
|
+
guardedBy: 'truncload.test.ts: a cover on the other arm of a branch is not narrowed',
|
|
248
|
+
rejects: (c) => !c.coveringDominates,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
/** One admitted-or-refused fold: the narrow load, the access it truncates, and what the gates see. */
|
|
253
|
+
export interface TruncLoadCandidate {
|
|
254
|
+
narrow: Access;
|
|
255
|
+
covering: Access;
|
|
256
|
+
c: TruncatedLoad;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Every narrow load an access at the same base covers, in program order.
|
|
260
|
+
*
|
|
261
|
+
* WHICH COVER, when several cover the same narrow read: the pick is a TOTAL ORDER over facts, not
|
|
262
|
+
* a reduction over the order the accesses happen to be emitted in. A cover the sound rules can
|
|
263
|
+
* admit outranks one they cannot — `{w4@8, w4@6, w1@8}` folds whichever of the two wide reads was
|
|
264
|
+
* emitted first otherwise, so an identical access set would admit or refuse on instruction
|
|
265
|
+
* scheduling alone. After that the WIDEST wins, because a base read at 4, 2 and 1 bytes from one
|
|
266
|
+
* offset collapses to ONE field in a single pass that way where the closest cover would need a
|
|
267
|
+
* fixpoint; then a load over a store; then the earlier access. A cover is never itself a
|
|
268
|
+
* candidate, since a candidate is strictly narrower than its own cover. */
|
|
269
|
+
export function truncatedLoadCandidates(fn: Fn, littleEndian: boolean): TruncLoadCandidate[] {
|
|
270
|
+
const { accessesOf } = constOffsetAccesses(fn);
|
|
271
|
+
const defs = defOpMap(fn);
|
|
272
|
+
const entryParams = new Set(fn.blocks[0].params);
|
|
273
|
+
const dom = dominators(fn);
|
|
274
|
+
const blockDominates = (a: number, b: number): boolean => dom.get(fn.blocks[b] as Block)!.has(fn.blocks[a] as Block);
|
|
275
|
+
const runsBefore = (a: Access, b: Access): boolean =>
|
|
276
|
+
a.block === b.block ? a.at < b.at : blockDominates(a.block, b.block);
|
|
277
|
+
const isLowOrderEnd = (narrow: Access, cover: Access): boolean =>
|
|
278
|
+
littleEndian ? narrow.off === cover.off : narrow.off + narrow.width === cover.off + cover.width;
|
|
279
|
+
|
|
280
|
+
const out: TruncLoadCandidate[] = [];
|
|
281
|
+
for (const b of fn.blocks) {
|
|
282
|
+
for (const op of b.ops as Op[]) {
|
|
283
|
+
if (op.opcode !== 'load') {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const base = op.operands[0];
|
|
287
|
+
const all = accessesOf.get(base)!;
|
|
288
|
+
const narrow = all.find((a) => a.op === op)!;
|
|
289
|
+
const covers = all.filter(
|
|
290
|
+
(a) => a.width > narrow.width && a.off <= narrow.off && a.off + a.width >= narrow.off + narrow.width,
|
|
291
|
+
);
|
|
292
|
+
if (covers.length === 0) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const preference = (a: Access): number[] => [
|
|
296
|
+
isLowOrderEnd(narrow, a) ? 1 : 0,
|
|
297
|
+
runsBefore(a, narrow) ? 1 : 0,
|
|
298
|
+
a.width,
|
|
299
|
+
a.isLoad ? 1 : 0,
|
|
300
|
+
];
|
|
301
|
+
const covering = covers.reduce((best, a) => {
|
|
302
|
+
const [x, y] = [preference(a), preference(best)];
|
|
303
|
+
const i = x.findIndex((v, k) => v !== y[k]);
|
|
304
|
+
return i >= 0 && x[i] > y[i] ? a : best;
|
|
305
|
+
});
|
|
306
|
+
out.push({
|
|
307
|
+
narrow,
|
|
308
|
+
covering,
|
|
309
|
+
c: {
|
|
310
|
+
fixedCell: constAddressOf(defs, base, narrow.off) !== null || globalCellOf(defs, base, narrow.off) !== null,
|
|
311
|
+
qualified: [op, covering.op].some(qualified),
|
|
312
|
+
resolvedBase: baseResolves(defs, entryParams, base),
|
|
313
|
+
coveringLoad: covering.isLoad,
|
|
314
|
+
lowOrderEnd: isLowOrderEnd(narrow, covering),
|
|
315
|
+
coveringDominates: runsBefore(covering, narrow),
|
|
316
|
+
castWidth: narrow.width * 8,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Rewrite every admitted narrow load into a cast of a wider one. Returns the number of rewrites.
|
|
325
|
+
*
|
|
326
|
+
* `gates` is a REQUIRED argument, not an option with a default: the table is how the refusals are
|
|
327
|
+
* ablated, and a second caller that could omit it would switch the channel off silently. */
|
|
328
|
+
export function foldTruncatedLoads(fn: Fn, littleEndian: boolean, gates: readonly Gate<TruncatedLoad>[]): number {
|
|
329
|
+
const admitted = truncatedLoadCandidates(fn, littleEndian).filter((k) => firstRejection(gates, k.c) === null);
|
|
330
|
+
if (admitted.length === 0) {
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
const byBlock = new Map<number, TruncLoadCandidate[]>();
|
|
334
|
+
for (const k of admitted) {
|
|
335
|
+
const list = byBlock.get(k.narrow.block);
|
|
336
|
+
if (list) {
|
|
337
|
+
list.push(k);
|
|
338
|
+
} else {
|
|
339
|
+
byBlock.set(k.narrow.block, [k]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const [block, ks] of byBlock) {
|
|
343
|
+
const b = fn.blocks[block];
|
|
344
|
+
const inserted = new Map<number, Op>();
|
|
345
|
+
for (const k of ks) {
|
|
346
|
+
const wide = mkOp('load', {
|
|
347
|
+
operands: [k.narrow.op.operands[0]],
|
|
348
|
+
results: [mkValue(T.unk(32))],
|
|
349
|
+
attrs: { off: k.covering.off, width: k.covering.width, signed: k.covering.signed },
|
|
350
|
+
});
|
|
351
|
+
inserted.set(k.narrow.at, wide);
|
|
352
|
+
// REWRITTEN IN PLACE, so the result `Value` identity survives and every existing use — and
|
|
353
|
+
// any slot home stamped on it (`ir/core.ts` SlotHomes) — keeps reading the same value. The
|
|
354
|
+
// operand it drops is the base, which the fresh load above still reads, so nothing is
|
|
355
|
+
// orphaned and the pass declares `dce: false`.
|
|
356
|
+
k.narrow.op.opcode = k.narrow.signed ? 'sext' : 'zext';
|
|
357
|
+
k.narrow.op.operands = [wide.results[0]];
|
|
358
|
+
k.narrow.op.attrs = { width: k.narrow.width * 8 };
|
|
359
|
+
}
|
|
360
|
+
b.ops = b.ops.flatMap((op, at) => {
|
|
361
|
+
const wide = inserted.get(at);
|
|
362
|
+
return wide ? [wide, op] : [op];
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return admitted.length;
|
|
366
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// asmlift — 64-bit runtime-helper lowering (L1 recognition).
|
|
2
|
+
//
|
|
3
|
+
// The peer of `raise/softdiv.ts`, one width up, and NOT gated on a hardware capability. A soft
|
|
4
|
+
// division is a division the ISA has no instruction for, so a target with a divider never emits
|
|
5
|
+
// one; no ISA this repo targets has 64-bit integer arithmetic at all, so `bl __muldi3` is software
|
|
6
|
+
// on every one of them whatever `hwDivide` says.
|
|
7
|
+
//
|
|
8
|
+
// By the time this runs the frontend has already read the call's arguments as 64-bit values and
|
|
9
|
+
// split its result back into the register pair (`frontend/thumb.ts`, at the `bl`), so the rewrite
|
|
10
|
+
// itself is the same one-line splice `softdiv` does: the helper's op over the same operands,
|
|
11
|
+
// reusing the SAME result value so every existing use already points at it.
|
|
12
|
+
//
|
|
13
|
+
// The signedness of the OPERATION comes from the helper name where the name splits it
|
|
14
|
+
// (`__divdi3`/`__udivdi3`) and from the operands where it does not — agbcc's `__muldi3` serves
|
|
15
|
+
// both spellings, so nothing here may read a signedness off it. See the table's own note.
|
|
16
|
+
import { Fn, mkOp } from '../ir/core';
|
|
17
|
+
import { arrivesAsDeclared, helperOp, isFloatHelper, isWideHelper, lookupHelper } from '../runtime-helpers';
|
|
18
|
+
import type { TargetDescription } from '../target';
|
|
19
|
+
|
|
20
|
+
/** Rewrite each recognised 64-bit helper call to the op it computes, in place. Returns whether
|
|
21
|
+
* anything changed. Runs BEFORE type recovery, so the operands get their signedness there. */
|
|
22
|
+
export function recognizeWideHelpers(fn: Fn, target: TargetDescription): boolean {
|
|
23
|
+
const table = target.runtimeHelpers ?? {};
|
|
24
|
+
let changed = false;
|
|
25
|
+
for (const b of fn.blocks) {
|
|
26
|
+
for (let i = 0; i < b.ops.length; i++) {
|
|
27
|
+
const op = b.ops[i];
|
|
28
|
+
if (op.opcode !== 'call') {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const helper = lookupHelper(table, String(op.attrs.target));
|
|
32
|
+
if (!helper?.op || !isWideHelper(helper) || isFloatHelper(helper)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// ITS C PARAMETERS AT THEIR STATED WIDTHS, not its argument registers and not their count:
|
|
36
|
+
// the frontend has paired the registers up, so a `__ashrdi3` that occupied three of them
|
|
37
|
+
// arrives here as a 64-bit value and a word. That pair construction is the evidence the fold
|
|
38
|
+
// rests on, and `arrivesAsDeclared` is the precondition for reading it — an operand count
|
|
39
|
+
// says nothing about what the operands hold, and a call carrying the right count of the
|
|
40
|
+
// wrong things folds into an operation over one half of each value.
|
|
41
|
+
//
|
|
42
|
+
// WHAT DECLINES HERE DOES NOT PASS THROUGH: `refuseUnmodelledHelpers` below gaps every
|
|
43
|
+
// surviving call to a name this table carries, so a shape this cannot fold gets the loud
|
|
44
|
+
// answer rather than a plausible one.
|
|
45
|
+
if (
|
|
46
|
+
!arrivesAsDeclared(
|
|
47
|
+
helper,
|
|
48
|
+
op.operands.map((o) => o.type),
|
|
49
|
+
op.results.map((r) => r.type),
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
b.ops.splice(i, 1, helperOp(helper.op, String(op.attrs.target), op.operands, op.results[0]));
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Turn every surviving call to one of the target's runtime helpers into a gap. Returns whether
|
|
62
|
+
* anything changed. Runs after the helper recognisers, so what it sees is what they declined.
|
|
63
|
+
*
|
|
64
|
+
* RE-EMITTING A COMPILER'S OWN RUNTIME CALL IS NOT A RECOVERY, and it is worse than a plain
|
|
65
|
+
* miss: it MATCHES. Hand `mwcceppc` the source `return __div2i(a, b);` and it emits the `bl
|
|
66
|
+
* __div2i` the row was lifted from, byte for byte — so the differ scores asmlift's failure to
|
|
67
|
+
* model 64-bit division exactly as it would score modelling it. A row that cannot tell the two
|
|
68
|
+
* apart is measuring nothing, and four cells of the synthetic 64-bit family were banking that.
|
|
69
|
+
*
|
|
70
|
+
* ONLY THE NAMES THE TARGET CARRIES, and a name outside the table stays an ordinary callee —
|
|
71
|
+
* which is right, because a project's own `__`-prefixed function is not this compiler's runtime
|
|
72
|
+
* and asmlift cannot tell them apart by spelling. A target with NO table therefore refuses
|
|
73
|
+
* nothing and spells every helper call it makes, which is the configuration this refusal exists
|
|
74
|
+
* to remove (`TargetDescription.runtimeHelpers`).
|
|
75
|
+
*
|
|
76
|
+
* An `opaque` rather than a throw, so the gap behaves like every other one: strict mode declines
|
|
77
|
+
* naming it, annotate mode marks it and leaves the rest of the function standing. */
|
|
78
|
+
export function refuseUnmodelledHelpers(fn: Fn, target: TargetDescription): boolean {
|
|
79
|
+
const table = target.runtimeHelpers;
|
|
80
|
+
if (!table) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
let changed = false;
|
|
84
|
+
for (const b of fn.blocks) {
|
|
85
|
+
for (let i = 0; i < b.ops.length; i++) {
|
|
86
|
+
const op = b.ops[i];
|
|
87
|
+
// Membership, through the table's one reader: `in` would also answer for `toString` and every
|
|
88
|
+
// other name on `Object.prototype`, and this is the refusal, so the fabricated reason would be
|
|
89
|
+
// the whole of what a caller saw.
|
|
90
|
+
if (op.opcode !== 'call' || lookupHelper(table, String(op.attrs.target)) === undefined) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
b.ops.splice(
|
|
94
|
+
i,
|
|
95
|
+
1,
|
|
96
|
+
mkOp('opaque', {
|
|
97
|
+
operands: [...op.operands],
|
|
98
|
+
results: [...op.results],
|
|
99
|
+
attrs: { helper: op.attrs.target },
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return changed;
|
|
106
|
+
}
|
package/src/rank-declare.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { type Fn, type Value, defOpMap } from './ir/core';
|
|
12
12
|
import type { SFn } from './l3/ast';
|
|
13
13
|
import { type SymbolRef, collectSymbolRefs } from './l3/symbol-refs';
|
|
14
|
-
import type {
|
|
15
|
-
import {
|
|
14
|
+
import type { Prototypes } from './proto';
|
|
15
|
+
import { type SymbolInfo, accessSignedness } from './symbols';
|
|
16
|
+
import { C_TYPEDEFS, type TargetDescription } from './target';
|
|
16
17
|
|
|
17
18
|
/** Bare-global ACCESS FACTS for name-only map symbols — the width/signedness authority the
|
|
18
19
|
* declaration synthesis (declare.ts) uses when the map has no shape. The map knows only the
|
|
@@ -42,7 +43,7 @@ export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; sign
|
|
|
42
43
|
} else {
|
|
43
44
|
a.widths.add(op.attrs.width as number);
|
|
44
45
|
if (op.opcode === 'load') {
|
|
45
|
-
a.signs.add((
|
|
46
|
+
a.signs.add(accessSignedness(op.attrs.width as number, op.attrs.signed as boolean | undefined));
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -202,15 +203,34 @@ export function makeRefCollector(ctx: {
|
|
|
202
203
|
/** the globals the TARGET names — the pool/reloc names and the shapes the asm evidences, before
|
|
203
204
|
* the map is unioned in */
|
|
204
205
|
targetNames: ReadonlySet<string>;
|
|
206
|
+
/** the project's prototype table — what lets a CALL TARGET be declared rather than refused */
|
|
207
|
+
prototypes: Prototypes;
|
|
208
|
+
/** the target the lift ran for, whose ABI says which declared types a printed prototype carries */
|
|
209
|
+
target: Pick<TargetDescription, 'doubleArgWords'>;
|
|
205
210
|
/** reports a refusal at most once per (name, reason); the caller owns the dedup */
|
|
206
211
|
refuse: (name: string, reason: RefusedDeclarationReason) => void;
|
|
207
212
|
}): (tree: SFn) => { symbolRefs?: SymbolRef[] } {
|
|
208
|
-
const { declSymbols, accessFacts, mapSymbols, targetNames, refuse } = ctx;
|
|
213
|
+
const { declSymbols, accessFacts, mapSymbols, targetNames, prototypes, target, refuse } = ctx;
|
|
209
214
|
return (tree: SFn): { symbolRefs?: SymbolRef[] } => {
|
|
210
215
|
// The names THIS tree binds. Computed per tree because the emitter mints local names per
|
|
211
216
|
// spelling — but the test below is NOT `bound` alone, and the difference is a wrong answer.
|
|
212
217
|
const bound = new Set<string>([...tree.params.map((p) => p.name), ...tree.locals.map((l) => l.name)]);
|
|
213
|
-
|
|
218
|
+
// A function-scope static is declared in the body beside the locals, so it shares their
|
|
219
|
+
// namespace: one the emitter's own names can reach dies with the spelling, as below.
|
|
220
|
+
const statics = new Set((tree.statics ?? []).map((st) => st.name));
|
|
221
|
+
for (const st of statics) {
|
|
222
|
+
if (bound.has(st) || EMITTER_NAME.test(st)) {
|
|
223
|
+
refuse(st, 'emitter-name');
|
|
224
|
+
throw new Error(
|
|
225
|
+
`cannot spell '${tree.name}': the target names a function-scope static '${st}', which is a ` +
|
|
226
|
+
`name the emitted C uses for its own locals and parameters — no declaration can bind it`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const refs = collectSymbolRefs(tree, declSymbols, prototypes, target, refuse).flatMap((r) => {
|
|
231
|
+
if (statics.has(r.name)) {
|
|
232
|
+
return []; // defined in the body — the map's global of the same name is another object
|
|
233
|
+
}
|
|
214
234
|
// THE ONE REFUSAL THAT IS NOT A REFUSAL — a name the emitted C uses for its OWN storage
|
|
215
235
|
// kills the SPELLING, because no declaration makes that candidate right and no declaration
|
|
216
236
|
// makes it fail either. Two shapes, and the second is why the test is the emitter's whole
|
|
@@ -257,6 +277,11 @@ export function makeRefCollector(ctx: {
|
|
|
257
277
|
// A ref no MAP accounts for is a hypothesis read out of the target asm, and it is marked
|
|
258
278
|
// as one all the way to the consumer (SymbolRef.synthesized).
|
|
259
279
|
const synthesized = mapSymbols?.has(r.name) ? {} : { synthesized: true as const };
|
|
280
|
+
// A global the asm reads narrower than a word holds no pointer, whatever it meets bare.
|
|
281
|
+
if (r.holdsPointer && access !== undefined && access.width !== 4) {
|
|
282
|
+
const { holdsPointer: _, ...integer } = r;
|
|
283
|
+
return [{ ...integer, access, ...synthesized }];
|
|
284
|
+
}
|
|
260
285
|
return [{ ...r, ...(access ? { access } : {}), ...synthesized }];
|
|
261
286
|
});
|
|
262
287
|
return refs.length ? { symbolRefs: refs } : {};
|
package/src/rank-variations.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import type { Gate } from './l3/gates';
|
|
28
28
|
import type { HoistPlacement } from './l3/hoist';
|
|
29
29
|
import { initFirstGuards } from './l3/initfirst';
|
|
30
|
+
import { narrowDeclarations, narrowReadDeclarations } from './l3/narrowdecl';
|
|
30
31
|
import { pollGuards, pollReads } from './l3/pollguard';
|
|
31
32
|
import { unmergeJoins } from './l3/unmerge';
|
|
32
33
|
// TYPE-ONLY, and deliberately: the structure-variation table types its `options` as
|
|
@@ -35,6 +36,7 @@ import { unmergeJoins } from './l3/unmerge';
|
|
|
35
36
|
import type { structureChecked } from './pipeline';
|
|
36
37
|
import {
|
|
37
38
|
hasDerivedReadHome,
|
|
39
|
+
hasEscapingExtension,
|
|
38
40
|
hasHomeableSharedAddress,
|
|
39
41
|
hasLoopSharedPureValue,
|
|
40
42
|
hasMergeFeedHome,
|
|
@@ -53,11 +55,12 @@ import type { VariationName } from './variation-tokens';
|
|
|
53
55
|
* that setting's own lifted fn (a map-lifted shared lift spells const bases as gaddr, which would
|
|
54
56
|
* blind the /raw-globals siblings — the /addr-home lesson). `strip` opts the variation into the
|
|
55
57
|
* dropped-sibling closure: a candidate that applies it is skipped when its default sibling (the
|
|
56
|
-
* same setting without it) failed the boundary contracts.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* same setting without it) failed the boundary contracts. Which of these entries `structure()`'s
|
|
59
|
+
* `assertDefaultAccepts` guard does NOT reset — the ones that cannot unlock a function the default
|
|
60
|
+
* declines, and so carry a written argument in place of the guard — is stated ONCE, beside that
|
|
61
|
+
* guard in structure.ts, and derived from the guard's own reset list in
|
|
62
|
+
* test/variation-offers.test.ts — never restated here, where a second copy drifts from the guard
|
|
63
|
+
* without anything going red. */
|
|
61
64
|
export interface StructureVariation {
|
|
62
65
|
flag:
|
|
63
66
|
| 'reread'
|
|
@@ -67,6 +70,7 @@ export interface StructureVariation {
|
|
|
67
70
|
| 'exprHome'
|
|
68
71
|
| 'derivedHome'
|
|
69
72
|
| 'mergeHome'
|
|
73
|
+
| 'escapeHome'
|
|
70
74
|
| 'unsCmp'
|
|
71
75
|
| 'freshMerge'
|
|
72
76
|
| 'copyDefPos'
|
|
@@ -203,6 +207,24 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
|
|
|
203
207
|
perLiftGate: hasMergeFeedHome,
|
|
204
208
|
strip: true,
|
|
205
209
|
},
|
|
210
|
+
// `/escape-home` — the escaping-extension-home variation (structure/analysis.ts AnalyzeOptions
|
|
211
|
+
// homeEscapingExtensions): a `zext`/`sext` with 2+ consumers, none of them in its own block,
|
|
212
|
+
// materializes into a local — the register the asm narrowed into once and every later block read
|
|
213
|
+
// — where the default re-evaluates the truncation at each consumer (`(u16)v` per use). The
|
|
214
|
+
// MIRROR of `/expr-home`, which wants a def outside a loop and a consumer inside; neither scope
|
|
215
|
+
// reaches the other's shape. Gated per symbol-map setting like its four siblings.
|
|
216
|
+
//
|
|
217
|
+
// A VARIATION, not a default, and `pokeemerald:AcroBikeHandleInputTurning:agbcc` is the row that
|
|
218
|
+
// decides it: it is a MATCH inside the scope, so forced on the spelling would be REPLACED across
|
|
219
|
+
// the fan rather than added to it. As a variation `compareScored` rides the un-homed sibling
|
|
220
|
+
// beside it and the match cannot be lost — the same argument `/merge-home` records above.
|
|
221
|
+
{
|
|
222
|
+
flag: 'escapeHome',
|
|
223
|
+
name: 'escape-home',
|
|
224
|
+
options: (on) => ({ homeEscapingExtensions: on }),
|
|
225
|
+
perLiftGate: hasEscapingExtension,
|
|
226
|
+
strip: true,
|
|
227
|
+
},
|
|
206
228
|
// `/uns-cmp` — spell unsigned compares unsigned (structure.ts unsignedCompareSpelling): an
|
|
207
229
|
// icmp_u* operand takes a (u32) cast where the rendered operands do not guarantee the
|
|
208
230
|
// unsignedness, and a mixed-claimant declaration reconciles to u32 when nothing under the
|
|
@@ -352,15 +374,21 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
|
|
|
352
374
|
];
|
|
353
375
|
|
|
354
376
|
/** The STACKED variations (sanctioned in the POLICY note at rank.ts's respell site): each entry is
|
|
355
|
-
* a statement-order/shape respell variation orthogonal to every
|
|
356
|
-
* onto every source.
|
|
377
|
+
* a statement-order/shape or declaration-width respell variation orthogonal to every respell
|
|
378
|
+
* variation outside this table, derived onto every source. Two members need not commute
|
|
379
|
+
* (`/narrow-decl` and `/narrow-read` compete for one cast), and table order then decides which one
|
|
380
|
+
* takes it in the all-together candidate. Each fires alone, plus
|
|
357
381
|
* all of them together in table order — not the full subset lattice; the pairs question is
|
|
358
382
|
* settled by applyStacked' skip-on-decline below, and a row demanding a true EXCLUSION pair —
|
|
359
|
-
*
|
|
383
|
+
* every member fires, the match needs a strict subset of them — is what would earn the lattice.
|
|
384
|
+
* So a member appended here also changes the all-together candidate wherever it fires beside two
|
|
385
|
+
* or more of the others. */
|
|
360
386
|
export const STACKED_VARIATIONS: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
|
|
361
387
|
{ name: 'initfirst', apply: initFirstGuards },
|
|
362
388
|
{ name: 'pollguard', apply: pollGuards },
|
|
363
389
|
{ name: 'pollread', apply: pollReads },
|
|
390
|
+
{ name: 'narrow-decl', apply: narrowDeclarations },
|
|
391
|
+
{ name: 'narrow-read', apply: narrowReadDeclarations },
|
|
364
392
|
];
|
|
365
393
|
|
|
366
394
|
/** The PRE-RESPELL variations (sanctioned in the POLICY note at rank.ts's respell site): a tree
|