@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
package/src/l3/ast.ts
CHANGED
|
@@ -15,7 +15,13 @@ export type Expr =
|
|
|
15
15
|
// `3 & (s32)p`). Scalar deref casts are backend-owned — the C-family printer synthesizes
|
|
16
16
|
// them from the `index` node's width. Each backend spells the cast in its own syntax
|
|
17
17
|
// (C: `(u8)e`; Pascal: no spelling yet → fails loud).
|
|
18
|
-
|
|
18
|
+
//
|
|
19
|
+
// `volatile` qualifies the POINTEE of a pointer cast (`(volatile u16 *)0x4000208`) — the one
|
|
20
|
+
// place asmlift can say "this access is to a volatile object" when there is no declaration to
|
|
21
|
+
// hang it on, which is exactly the raw-address case (l3/inlinebase.ts). IrType models no
|
|
22
|
+
// cv-qualifier, deliberately: volatility is a SPELLING, carried at the declaration or the
|
|
23
|
+
// cast, the same split SFn.locals makes for `volatile`/`pointeeVolatile`.
|
|
24
|
+
| { k: 'cast'; to: IrType; e: Expr; volatile?: true }
|
|
19
25
|
| { k: 'call'; fn: string; args: Expr[] }
|
|
20
26
|
// The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
|
|
21
27
|
// DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
|
|
@@ -38,13 +44,68 @@ export type Expr =
|
|
|
38
44
|
// Variable-index `a[i]` is recovered at the IR level (`aload`/`astore` carry elemSize;
|
|
39
45
|
// raise/arrays.ts) but still LOWERS to this one C-shaped `index` node, so it stays C-only
|
|
40
46
|
// (a Pascal array-access spelling is future work). Treat `index` with idx ≠ 0 as C-shaped.
|
|
41
|
-
// `lead` prefixes
|
|
42
|
-
// exactly one inhabitant: the bare-name spelling of a MULTIDIMENSIONAL
|
|
43
|
-
// subscript reaches a row and the element needs the leading dimensions
|
|
44
|
-
// still denotes ONE `width`-byte element, so its type, its legalization
|
|
45
|
-
// are unchanged — this is a spelling of the same address, not a new kind
|
|
46
|
-
// every rank-1 access, which is why it is optional rather than an empty
|
|
47
|
-
|
|
47
|
+
// `lead` prefixes the LEADING subscripts before `idx` — `g[0][i]` or `g[r][i]` rather than
|
|
48
|
+
// `g[i]`. It exists for exactly one inhabitant: the bare-name spelling of a MULTIDIMENSIONAL
|
|
49
|
+
// array global, where one subscript reaches a row and the element needs the leading dimensions
|
|
50
|
+
// pinned first. The node still denotes ONE `width`-byte element, so its type, its legalization
|
|
51
|
+
// and its stride contract are unchanged — this is a spelling of the same address, not a new kind
|
|
52
|
+
// of access. Absent for every rank-1 access, which is why it is optional rather than an empty
|
|
53
|
+
// array. A leading subscript is an EXPRESSION because a row index the asm computed is a value,
|
|
54
|
+
// not a literal (`g[gRow][i]`), so every generic walk descends into it: a name mentioned there
|
|
55
|
+
// is a real use, and a rewrite that skipped it would rename half an address. `exprChildren` and
|
|
56
|
+
// `mapExprChildren` below carry it, which is what makes that true for every walk DERIVED from
|
|
57
|
+
// them; the one walk that is not — l3/mentions.ts, which hand-rolls a traversal to tell an
|
|
58
|
+
// `index` BASE from every other position — enumerates the positions itself and has to be
|
|
59
|
+
// extended by hand when one is added here.
|
|
60
|
+
// `operandOff` is the one field here that is EVIDENCE rather than spelling: it is the BYTE
|
|
61
|
+
// DISPLACEMENT this access's constant offset arrived in through the instruction's MEMORY
|
|
62
|
+
// OPERAND (`ldrb [r0, #0x3]` records 3), as opposed to through the address the pool word
|
|
63
|
+
// materialized (`.word gSym+0x3`, which records nothing). Both denote the same cell and print
|
|
64
|
+
// the same subscript, which is why `exprEquals` ignores it — but on a compiler that folds a
|
|
65
|
+
// constant subscript into the literal, only one C spelling could have put it there, so
|
|
66
|
+
// `l3/basecse.ts` reads its PRESENCE as the evidence its `unfoldedOffset` rule is about and
|
|
67
|
+
// `l3/offmember.ts` reads its VALUE as the member offset to re-spell. Absent whenever the
|
|
68
|
+
// offset was 0 or came from the address expression, so absence is never proof of anything — and
|
|
69
|
+
// a displacement can be NEGATIVE, so every reader tests `!== undefined`, never truthiness.
|
|
70
|
+
//
|
|
71
|
+
// THE FIELD IS HALF OF A TWO-PART FACT, and the other half has no field because it needs none.
|
|
72
|
+
// A displacement that reached the instruction says the compiler did not fold it; WHAT TO DO
|
|
73
|
+
// about that depends on the BASE'S KIND, which is read off the base expression at each reader:
|
|
74
|
+
// • a local (`var`) — nothing to reassociate into; already right, and no reader fires.
|
|
75
|
+
// • a plus tree — the fold pulls the displacement into the tree, costing an `add` and a
|
|
76
|
+
// register. The repair is to home the base in a local so it becomes a
|
|
77
|
+
// `var`, which is the ADDRESS-HOME axis and lives one level down at L2
|
|
78
|
+
// (`structure/analysis.ts`'s `sharedBaseClasses`) because materializing
|
|
79
|
+
// a value is a structuring decision, not a spelling.
|
|
80
|
+
// • a leaf const/addr — the fold bakes the displacement into the literal, changing the `.word`.
|
|
81
|
+
// The repair is a spelling: `/basefold`'s named base or
|
|
82
|
+
// `/offmember`'s aggregate member, both at L3.
|
|
83
|
+
// The two repairs are deliberately NOT dispatched from one place. They sit at different levels
|
|
84
|
+
// and partition the space by their own pre-existing predicates (a non-const pure def with no
|
|
85
|
+
// gaddr in its cone, versus `l3/offmember.ts`'s leaf base), so a shared dispatch would be
|
|
86
|
+
// scaffolding over two rules that already disagree about what a base is. What the reader needs
|
|
87
|
+
// is the map, which is this paragraph.
|
|
88
|
+
//
|
|
89
|
+
// `baseOrdered` is the SECOND evidence field, and it answers about the base rather than the
|
|
90
|
+
// offset: the input assembly materialized this access's base BEFORE it scaled the index
|
|
91
|
+
// (raise/globalshape.ts `orderLicensedGlobals`, stamped at the structure seam; that module's
|
|
92
|
+
// header carries the compiles saying which source spellings produce which order). It is what
|
|
93
|
+
// says a base may be given a HOME — `l3/basecse.ts`'s `order-licensed`. Absent means "no such
|
|
94
|
+
// evidence", never "the index came first": a scaling in another block is not comparable, and a
|
|
95
|
+
// compiler that has not opted in stamps nothing. `exprEquals` ignores it for `operandOff`'s
|
|
96
|
+
// reason — both spellings denote the same cell — and it is per SYMBOL, so every access of one
|
|
97
|
+
// name carries the same answer, which is right for agbcc because one CSEd pool load serves
|
|
98
|
+
// them all.
|
|
99
|
+
| {
|
|
100
|
+
k: 'index';
|
|
101
|
+
base: Expr;
|
|
102
|
+
idx: Expr;
|
|
103
|
+
width: number;
|
|
104
|
+
signed: boolean;
|
|
105
|
+
lead?: Expr[];
|
|
106
|
+
operandOff?: number;
|
|
107
|
+
baseOrdered?: true;
|
|
108
|
+
}
|
|
48
109
|
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
49
110
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
50
111
|
// Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
|
|
@@ -59,31 +120,41 @@ export type Expr =
|
|
|
59
120
|
// default) never produces this node; it keeps the `"?"` sentinel → ContractError behavior.
|
|
60
121
|
| { k: 'marker'; reason: string; args: Expr[] };
|
|
61
122
|
|
|
62
|
-
// `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
123
|
+
// THE SIGNEDNESS-CARRYING PAIRS. `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one;
|
|
124
|
+
// `/`/`%` are the SIGNED quotient and remainder and `/u`/`%u` the unsigned ones. C spells each pair
|
|
125
|
+
// with one token and picks between them from the operand types, so the C backend synthesizes the
|
|
126
|
+
// cast that pins the choice — exactly as it already synthesizes scalar deref casts from an `index`
|
|
127
|
+
// node's width. A backend with no spelling for one of them (IDO Pascal) declines LOUDLY on the
|
|
128
|
+
// operation itself, rather than on whatever artifact another language's spelling happened to leave
|
|
129
|
+
// in the tree.
|
|
130
|
+
//
|
|
131
|
+
// WHY THESE SPLITS AND NOT THE OTHERS. "The machine distinguishes them" is NOT the rule — the
|
|
132
|
+
// machine distinguishes `sltu`/`slt` too, and CMP_TO_BIN deliberately collapses `icmp_u*`→`<` etc.,
|
|
133
|
+
// noting that "unsignedness is in the operand types". Taking the machine as the rule would license
|
|
134
|
+
// more splits with no inhabitant, which is what "earn the level" forbids. The rule is the repo's
|
|
135
|
+
// own: a split is earned by a real, byte-load-bearing divergence WITH inhabitants that no other
|
|
136
|
+
// channel can carry. The shifts earned it first (~20 rows, 5 projects, 4 compilers) because the
|
|
137
|
+
// operand type could not carry them — a promoted narrow value is signed whatever it was loaded as.
|
|
67
138
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// channel could carry it — the operand type could not, since a promoted narrow value is signed
|
|
75
|
-
// whatever it was loaded as.
|
|
139
|
+
// The divides earned it second, on pokeemerald:GetAnchorCoord — `(u32)(coord * a1) / (u32)a0`
|
|
140
|
+
// standing beside two arithmetic shifts of the same values. Their only other channel is the operand
|
|
141
|
+
// TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
|
|
142
|
+
// once: the divisor also feeds a signed compare, so the /uns-cmp reconciliation correctly refuses
|
|
143
|
+
// it, and forcing it anyway makes agbcc delete the comparison as always-false. A per-operand pin is
|
|
144
|
+
// the only spelling that says "this division alone is unsigned".
|
|
76
145
|
//
|
|
77
|
-
// The
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
146
|
+
// The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
|
|
147
|
+
// which side a compare was spelled from genuinely underdetermines — a signed spelling that
|
|
148
|
+
// byte-matched was proved non-negative by the compiler — so it is refereed as an axis, while a
|
|
149
|
+
// division helper is a pure function of the expression's C type with no such proof available.
|
|
81
150
|
export type BinOp =
|
|
82
151
|
| '+'
|
|
83
152
|
| '-'
|
|
84
153
|
| '*'
|
|
85
154
|
| '/'
|
|
155
|
+
| '/u'
|
|
86
156
|
| '%'
|
|
157
|
+
| '%u'
|
|
87
158
|
| '<'
|
|
88
159
|
| '<='
|
|
89
160
|
| '>'
|
|
@@ -130,7 +201,20 @@ export type Stmt =
|
|
|
130
201
|
| { k: 'continue' }
|
|
131
202
|
// A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
|
|
132
203
|
// a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
|
|
133
|
-
// is emitted
|
|
204
|
+
// is emitted after `defaultAt` of them, or after all of them when that is absent.
|
|
205
|
+
//
|
|
206
|
+
// `defaultAt` exists because C lets `default:` sit BETWEEN case labels and a compiler that lays
|
|
207
|
+
// case bodies out in source order shows where the source put it. It is a COUNT of preceding arms,
|
|
208
|
+
// not an index into an array a later pass may rebuild, and a count past the arms is a producer bug
|
|
209
|
+
// a backend refuses. Setting it is legal only when the arm before the label does not fall through:
|
|
210
|
+
// moving the label in front of a falling arm would divert that arm into the default. The C-family
|
|
211
|
+
// printer terminates a non-final default with `break;` for the mirror-image reason.
|
|
212
|
+
//
|
|
213
|
+
// Unlike `fallsThrough` below, `defaultAt` is a SPELLING: the arm BEFORE the label is closed (the
|
|
214
|
+
// rule above) and so is the default body itself (the C-family printer terminates a non-final one),
|
|
215
|
+
// so a backend with no positional default (Pascal's `otherwise`) may ignore the count and still
|
|
216
|
+
// emit the same program. The arm AFTER the label is under no such rule and MAY fall through — it
|
|
217
|
+
// falls into the arm below it, which the label does not stand between.
|
|
134
218
|
//
|
|
135
219
|
// NON-NEUTRALITY NOTE (like the `index` node above): `fallsThrough` encodes a C/C++ control-flow
|
|
136
220
|
// concept POSITIONALLY — `cases[i].fallsThrough === true` means control continues into
|
|
@@ -139,7 +223,12 @@ export type Stmt =
|
|
|
139
223
|
// Pascal backend MUST loud-fail a `fallsThrough` case (it has no faithful spelling), exactly as it
|
|
140
224
|
// loud-fails `field`/`cast`. Recovery must therefore only set `fallsThrough` when the fall-through
|
|
141
225
|
// target is the emission-adjacent case.
|
|
142
|
-
|
|
226
|
+
//
|
|
227
|
+
// Recovery COMPUTES this flag rather than spelling it, so a source grep for `fallsThrough: true`
|
|
228
|
+
// finds hand-written fixtures and nothing else, whatever the corpus does — count its inhabitants
|
|
229
|
+
// by instrumenting the printer. Both regimes produce them: the jump table spells `case 4:` of
|
|
230
|
+
// `kleod:UpdateWorldMapNodeAnim`, the comparison tree `synthetic:sw_fallmem:agbcc`.
|
|
231
|
+
| { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[]; defaultAt?: number }
|
|
143
232
|
| { k: 'return'; value?: Expr };
|
|
144
233
|
|
|
145
234
|
/** One arm of a `switch`. `values` stacks multiple `case K:` labels onto one body (`case 1: case 2:`).
|
|
@@ -153,7 +242,59 @@ export interface SwitchCase {
|
|
|
153
242
|
export interface SFn {
|
|
154
243
|
name: string;
|
|
155
244
|
params: { name: string; type: IrType }[];
|
|
156
|
-
|
|
245
|
+
/** Recovered locals, declared at function top. Two INDEPENDENT volatility facts, mirroring
|
|
246
|
+
* symbols.ts's cell-vs-pointee split: `volatile` = the local OBJECT is volatile (the
|
|
247
|
+
* address-escaped frame scratch; dce.ts treats reads of it as observable), `pointeeVolatile`
|
|
248
|
+
* = the local is a pointer TO volatile data (the l3/volatileptr.ts lever; a declaration
|
|
249
|
+
* spelling only — nothing about the local itself is observable).
|
|
250
|
+
*
|
|
251
|
+
* `frame` is present on a local the structurer recovered from an `laddr` — the asm
|
|
252
|
+
* MATERIALIZED the slot's address into a register, so the object provably lives in memory —
|
|
253
|
+
* and carries the machine's static access counts for it. Under Thumb that envelope is a
|
|
254
|
+
* SUB-WORD frame object: `strh/ldrh/strb/ldrb` have no `[sp,#imm]` form, so a compiler must
|
|
255
|
+
* copy `sp` first, while a word spill goes straight to `[sp,#imm]` and is recovered as an
|
|
256
|
+
* SSA value with no local of its own. So `frame` is NOT the set of every value the machine
|
|
257
|
+
* slotted. `loads`/`stores` are the yardstick a qualifier lever must match before it may
|
|
258
|
+
* declare every access to the object observable: the readability passes between here and L3
|
|
259
|
+
* may drop a store or render one machine load as two reads, and `volatile` over an access
|
|
260
|
+
* set asmlift did not preserve is a source that contradicts itself. ABSENT where the counts
|
|
261
|
+
* would be a floor rather than the set: an address reaching anything but a direct load/store
|
|
262
|
+
* leaves accesses the count cannot see.
|
|
263
|
+
*
|
|
264
|
+
* ORDER MATTERS, and it means two different things at two different times. As the structurer
|
|
265
|
+
* builds it and as every L3 pass sees it, this is the RECOVERED DECLARATION ORDER — the naming
|
|
266
|
+
* walk's order — and passes reason about it as such: `l3/coalesce.ts` picks the arm-disjoint
|
|
267
|
+
* survivor by position in THIS list, so the earlier declaration wins the way a shared source
|
|
268
|
+
* local reads. The EMITTED order is this list re-sorted by `l3/slotorder.ts` inside `emit`,
|
|
269
|
+
* which happens after every pass and returns a copy. A pass that sorted the list any earlier
|
|
270
|
+
* would silently change which local survives every arm-disjoint merge.
|
|
271
|
+
*
|
|
272
|
+
* ONE COMMENT, DELIBERATELY. Five sites tell a reader to consult "the SFn.locals doc"
|
|
273
|
+
* (structure/structure.ts, l3/inlinebase.ts, l3/volatileval.ts, l3/reindex.ts, l3/unreduce.ts),
|
|
274
|
+
* and TypeScript attaches only the LAST doc block before a declaration — so a second block
|
|
275
|
+
* added in front of this one would silently orphan everything above and those five references
|
|
276
|
+
* would point at half a paragraph. Append here; do not add a neighbour. */
|
|
277
|
+
locals: {
|
|
278
|
+
name: string;
|
|
279
|
+
type: IrType;
|
|
280
|
+
volatile?: true;
|
|
281
|
+
pointeeVolatile?: true;
|
|
282
|
+
frame?: { loads: number; stores: number };
|
|
283
|
+
/** the local stands on an `undef` — storage the asm reads without ever writing it, where the
|
|
284
|
+
* MISSING assignment is the recovery. Marked because a local read and never assigned is
|
|
285
|
+
* otherwise a dropped statement (contracts.ts assertLocalsWritten). */
|
|
286
|
+
uninit?: true;
|
|
287
|
+
/** every `[sp,#k]` the machine homed this local at, when the asm spilled it — ascending, and
|
|
288
|
+
* usually one. Several when the naming walk put several spilled values under this one name,
|
|
289
|
+
* or a coalesce absorbed a second homed local into it; the list is the UNION and picks
|
|
290
|
+
* nothing, because which offset is the earlier DECLARATION RANK depends on the frame's
|
|
291
|
+
* direction and only `l3/slotorder.ts` holds it (ir/core.ts `SlotHomes`).
|
|
292
|
+
*
|
|
293
|
+
* Present ONLY on a local recovered from word-spill VALUES. A `frame` local and a `uninit`
|
|
294
|
+
* one are deliberately left unstamped even where the offset is in hand — see the refusal at
|
|
295
|
+
* the structurer's build site for the measurement that decided it. */
|
|
296
|
+
slots?: number[];
|
|
297
|
+
}[];
|
|
157
298
|
/** project globals referenced with a known declaration shape (symbol map) — typed for the
|
|
158
299
|
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
159
300
|
* declare them, exactly like every other global name asmlift emits. */
|
|
@@ -163,6 +304,18 @@ export interface SFn {
|
|
|
163
304
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
164
305
|
* unless raise/structs.ts recovered a struct. Sorted by name for deterministic output. */
|
|
165
306
|
structs?: StructType[];
|
|
307
|
+
/** Which way this compiler hands out frame slots against DECLARATION RANK, when it is known:
|
|
308
|
+
*
|
|
309
|
+
* THE ONE COMPILER DATUM ON THE NEUTRAL TREE, and it is here rather than in a backend on
|
|
310
|
+
* purpose: `LanguageBackend.emit(fn: SFn): string` is the only seam a backend has, and widening
|
|
311
|
+
* it to `emit(fn, opts)` would hand the datum back to the seven `.emit(` CALL SITES this design
|
|
312
|
+
* exists to keep it out of. So the tree is neutral in its NODES — every node still spells the
|
|
313
|
+
* same thing in C, C++ and Pascal — and not in its emission policy, which this field is.
|
|
314
|
+
* `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Set by the
|
|
315
|
+
* structurer from `StructureOptions.spillSlotOrder`, itself a per-compiler default declared in
|
|
316
|
+
* `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
|
|
317
|
+
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
318
|
+
slotOrder?: 'ascending' | 'descending';
|
|
166
319
|
}
|
|
167
320
|
|
|
168
321
|
/** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
|
|
@@ -177,6 +330,14 @@ export interface StructType {
|
|
|
177
330
|
* comment spelling. */
|
|
178
331
|
export interface LanguageBackend {
|
|
179
332
|
readonly id: 'c' | 'cpp' | 'pascal';
|
|
333
|
+
/** Can this language spell a `switch` arm that RUNS ON into the next one (`fallsThrough`)?
|
|
334
|
+
* C and C++ can; Pascal's `case-of` cannot, and its backend loud-fails the node (see the
|
|
335
|
+
* non-neutrality note on `Stmt`'s `switch`). Declared here rather than inferred from `id`
|
|
336
|
+
* because it is what RECOVERY must ask: a comparison-tree switch has a second, behaviourally
|
|
337
|
+
* identical recovery (plain if-nesting), so minting a `fallsThrough` arm for a backend that
|
|
338
|
+
* cannot print it turns a whole function that used to decompile into a loud stub. The
|
|
339
|
+
* structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
|
|
340
|
+
readonly spellsSwitchFallthrough: boolean;
|
|
180
341
|
emit(fn: SFn): string;
|
|
181
342
|
// Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
|
|
182
343
|
// Used by the annotate-mode stub path to carry the failure reason + the original asm
|
|
@@ -201,8 +362,17 @@ export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
|
|
|
201
362
|
}
|
|
202
363
|
|
|
203
364
|
/** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
|
|
204
|
-
* fieldSpellsDot/derefStrideOk)
|
|
205
|
-
*
|
|
365
|
+
* fieldSpellsDot/derefStrideOk), exhaustive under noImplicitReturns like the walkers below.
|
|
366
|
+
*
|
|
367
|
+
* Key-order-independent for every EXPR field — a switch, not a stringify. NOT for the `cast`
|
|
368
|
+
* arm's TARGET TYPE, which is compared by serialization and so is order-sensitive: two `IrType`
|
|
369
|
+
* objects with the same fields written in a different order compare UNEQUAL. That is why the
|
|
370
|
+
* inline `IrType` literals elsewhere in the codebase are written in `T.int`/`T.ptr` key order —
|
|
371
|
+
* matching the constructors keeps them comparable against constructed types.
|
|
372
|
+
*
|
|
373
|
+
* `typeEquals` (ir/types.ts) is NOT the fix: it ignores `struct.size`, so it is strictly more
|
|
374
|
+
* permissive, and swapping it in here would let a CSE collapse two accesses whose struct stride
|
|
375
|
+
* differs. */
|
|
206
376
|
export function exprEquals(a: Expr, b: Expr): boolean {
|
|
207
377
|
if (a.k !== b.k) {
|
|
208
378
|
return false;
|
|
@@ -224,7 +394,14 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
224
394
|
}
|
|
225
395
|
case 'cast': {
|
|
226
396
|
const bb = b as typeof a;
|
|
227
|
-
|
|
397
|
+
// `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
|
|
398
|
+
// CSE or dedup that treats these as equal keeps one node and drops the other, silently
|
|
399
|
+
// respelling a volatile access as a plain one.
|
|
400
|
+
return (
|
|
401
|
+
JSON.stringify(a.to) === JSON.stringify(bb.to) &&
|
|
402
|
+
(a.volatile ?? false) === (bb.volatile ?? false) &&
|
|
403
|
+
exprEquals(a.e, bb.e)
|
|
404
|
+
);
|
|
228
405
|
}
|
|
229
406
|
case 'call': {
|
|
230
407
|
const bb = b as typeof a;
|
|
@@ -234,13 +411,16 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
234
411
|
const bb = b as typeof a;
|
|
235
412
|
// `lead` is part of the ADDRESS (`g[0][i]` and `g[1][i]` are different elements), so it
|
|
236
413
|
// must be compared — an omission here would let CSE/dedup collapse two distinct accesses.
|
|
414
|
+
// `operandOff` deliberately is NOT: two accesses agreeing on everything else denote the same
|
|
415
|
+
// cell and print the same subscript however the machine spelled the offset, so a CSE that
|
|
416
|
+
// collapses them respells nothing.
|
|
237
417
|
const lead = a.lead ?? [];
|
|
238
418
|
const bLead = bb.lead ?? [];
|
|
239
419
|
return (
|
|
240
420
|
a.width === bb.width &&
|
|
241
421
|
a.signed === bb.signed &&
|
|
242
422
|
lead.length === bLead.length &&
|
|
243
|
-
lead.every((v, i) => v
|
|
423
|
+
lead.every((v, i) => exprEquals(v, bLead[i])) &&
|
|
244
424
|
exprEquals(a.base, bb.base) &&
|
|
245
425
|
exprEquals(a.idx, bb.idx)
|
|
246
426
|
);
|
|
@@ -263,14 +443,6 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
263
443
|
}
|
|
264
444
|
}
|
|
265
445
|
|
|
266
|
-
// ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
|
|
267
|
-
// Every generic walker derives from these helpers, so a NEW node kind is a compile error in
|
|
268
|
-
// exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
|
|
269
|
-
// hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
|
|
270
|
-
// SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
|
|
271
|
-
// recognizeForLoops) rightly keep their own switches.
|
|
272
|
-
|
|
273
|
-
/** The direct sub-expressions of `e`, in syntactic order. */
|
|
274
446
|
/** THE spelling of an unmodelled instruction's gap reason, in one place: `structure.ts` writes it
|
|
275
447
|
* into the marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
|
|
276
448
|
* classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
|
|
@@ -279,6 +451,14 @@ export function gapReasonFor(mnemonic: unknown): string {
|
|
|
279
451
|
return `unmodelled instruction '${typeof mnemonic === 'string' ? mnemonic : '?'}'`;
|
|
280
452
|
}
|
|
281
453
|
|
|
454
|
+
// ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
|
|
455
|
+
// Every generic walker derives from these helpers, so a NEW node kind is a compile error in
|
|
456
|
+
// exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
|
|
457
|
+
// hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
|
|
458
|
+
// SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
|
|
459
|
+
// recognizeForLoops) rightly keep their own switches.
|
|
460
|
+
|
|
461
|
+
/** The direct sub-expressions of `e`, in syntactic order. */
|
|
282
462
|
export function exprChildren(e: Expr): Expr[] {
|
|
283
463
|
switch (e.k) {
|
|
284
464
|
case 'var':
|
|
@@ -293,7 +473,7 @@ export function exprChildren(e: Expr): Expr[] {
|
|
|
293
473
|
case 'call':
|
|
294
474
|
return e.args;
|
|
295
475
|
case 'index':
|
|
296
|
-
return [e.base, e.idx];
|
|
476
|
+
return [e.base, ...(e.lead ?? []), e.idx];
|
|
297
477
|
case 'field':
|
|
298
478
|
return [e.base];
|
|
299
479
|
case 'marker':
|
|
@@ -316,7 +496,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
|
|
|
316
496
|
case 'call':
|
|
317
497
|
return { ...e, args: e.args.map(f) };
|
|
318
498
|
case 'index':
|
|
319
|
-
return { ...e, base: f(e.base), idx: f(e.idx) };
|
|
499
|
+
return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
|
|
320
500
|
case 'field':
|
|
321
501
|
return { ...e, base: f(e.base) };
|
|
322
502
|
case 'marker':
|
|
@@ -349,7 +529,132 @@ export function stmtExprs(s: Stmt): Expr[] {
|
|
|
349
529
|
}
|
|
350
530
|
}
|
|
351
531
|
|
|
352
|
-
/**
|
|
532
|
+
/** Rebuild `s` with EVERY expression position mapped through `f` — store lvalues and loop/switch
|
|
533
|
+
* heads included, nested statements recursively. The rewrite dual of stmtExprs/stmtChildren, for
|
|
534
|
+
* the PURE 1:1 case: one expression in, one expression out, every statement kept.
|
|
535
|
+
*
|
|
536
|
+
* A LEVER THAT HAND-ROLLS ITS OWN MAPPER IS NOT A MISSED MIGRATION. Several do, because their
|
|
537
|
+
* contract is not this one — a rewrite that may DECLINE, one that INSERTS statements, one that
|
|
538
|
+
* rewrites assign TARGETS as well as expressions, one that turns a statement into a list. Check
|
|
539
|
+
* the contract before pointing one of them here. */
|
|
540
|
+
export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
|
|
541
|
+
const mapS = (x: Stmt): Stmt => mapStmtExprs(x, f);
|
|
542
|
+
switch (s.k) {
|
|
543
|
+
case 'assign':
|
|
544
|
+
return { ...s, value: f(s.value) };
|
|
545
|
+
case 'store':
|
|
546
|
+
return { ...s, lval: f(s.lval), value: f(s.value) };
|
|
547
|
+
case 'exprstmt':
|
|
548
|
+
return { ...s, value: f(s.value) };
|
|
549
|
+
case 'return':
|
|
550
|
+
return s.value ? { ...s, value: f(s.value) } : s;
|
|
551
|
+
case 'if':
|
|
552
|
+
return { ...s, cond: f(s.cond), then: s.then.map(mapS), else: s.else.map(mapS) };
|
|
553
|
+
case 'while':
|
|
554
|
+
case 'dowhile':
|
|
555
|
+
return { ...s, cond: f(s.cond), body: s.body.map(mapS) };
|
|
556
|
+
case 'for':
|
|
557
|
+
return { ...s, init: mapS(s.init), cond: f(s.cond), inc: mapS(s.inc), body: s.body.map(mapS) };
|
|
558
|
+
case 'switch':
|
|
559
|
+
return {
|
|
560
|
+
...s,
|
|
561
|
+
scrutinee: f(s.scrutinee),
|
|
562
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(mapS) })),
|
|
563
|
+
default: s.default?.map(mapS),
|
|
564
|
+
};
|
|
565
|
+
case 'break':
|
|
566
|
+
case 'continue':
|
|
567
|
+
return s;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** An address the target can REMATERIALIZE: a constant expression, reading no variable and no
|
|
572
|
+
* memory. Which ENCODING the compiler picked for it is not a property of the source — agbcc
|
|
573
|
+
* spells a pool word `(s32 *)33569456` but a shift-encodable one `(s32 *)(128 << 18)`, and every
|
|
574
|
+
* GBA hardware region (EWRAM 0x2000000, I/O 0x4000000, VRAM 0x6000000 …) takes the second form —
|
|
575
|
+
* so both must reach the same admission or the whole MMIO/VRAM fill family declines on its
|
|
576
|
+
* address. A bare `(T *)0` is excluded — a null base is not a walk — but the test is on the
|
|
577
|
+
* LITERALS the expression mentions, not on the value they fold to, so `(T *)(5 - 5)` passes.
|
|
578
|
+
* Folding would need a constant evaluator no consumer has another use for, and both consumers
|
|
579
|
+
* keep the expression verbatim, so no decision downstream reads the value.
|
|
580
|
+
*
|
|
581
|
+
* Two ask it: the walk re-index (l3/reindex.ts) about a walk base, and the `volatile` qualifier
|
|
582
|
+
* (l3/volatileptr.ts) about what feeds a pointer local. They must agree — a MMIO fill whose base
|
|
583
|
+
* one admits and the other refuses can be re-indexed but never qualified, so the paired
|
|
584
|
+
* `/indexed/volatile` spelling is unreachable at exactly the hardware addresses it is for.
|
|
585
|
+
*
|
|
586
|
+
* Two levers reading the same initializers are deliberately NOT here: l3/inlinebase.ts
|
|
587
|
+
* substitutes the address at each use, l3/nearbase.ts clusters neighbours by distance, and both
|
|
588
|
+
* need the VALUE, which is the evaluator above. Declining a shift-encoded base there costs a
|
|
589
|
+
* lever that does not fire, and the population is small: over klonoa's 531 lifting functions,
|
|
590
|
+
* one inlinebase-shaped local with no symbol map and none with it; a folded nearbase would form
|
|
591
|
+
* a new cluster in 4 functions mapless and 1 with the map. Both counts were zero over the agbcc
|
|
592
|
+
* benchmark rows that lifted when the levers landed — a MEASUREMENT over a corpus that grows, so
|
|
593
|
+
* re-take it rather than quoting it: a new row falsifies the number, not the argument. */
|
|
594
|
+
export function rematerializableAddress(e: Expr): boolean {
|
|
595
|
+
let nonZero = false;
|
|
596
|
+
let ok = true;
|
|
597
|
+
const visit = (x: Expr): void => {
|
|
598
|
+
switch (x.k) {
|
|
599
|
+
case 'const':
|
|
600
|
+
nonZero ||= x.value !== 0;
|
|
601
|
+
break;
|
|
602
|
+
case 'cast':
|
|
603
|
+
case 'bin':
|
|
604
|
+
case 'un':
|
|
605
|
+
break;
|
|
606
|
+
default:
|
|
607
|
+
ok = false; // var, addr, index, field, call, marker
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
for (const c of exprChildren(x)) {
|
|
611
|
+
visit(c);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
visit(e);
|
|
615
|
+
return ok && nonZero;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, or a marker
|
|
619
|
+
* standing in for an unmodelled instruction (annotate mode). */
|
|
620
|
+
export function exprHasEffect(e: Expr): boolean {
|
|
621
|
+
return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
|
|
625
|
+
* move, and the one `exprHasEffect` deliberately does not answer: that one is about a call, this
|
|
626
|
+
* about a QUALIFIER, and a pass that asks the first where it means the second reorders device
|
|
627
|
+
* accesses while its gate reports clean.
|
|
628
|
+
*
|
|
629
|
+
* Three spellings assert one thing, and all three are here because a lever that knew only the cast
|
|
630
|
+
* would miss the two a later lever writes: a `volatile` cast (where a raw address carries it), a
|
|
631
|
+
* read through a pointer local declared to point at volatile data (l3/volatileptr.ts), and a read
|
|
632
|
+
* of a `volatile` local object (l3/volatileval.ts). A bare cast counts even with no deref under it
|
|
633
|
+
* — the qualifier is on the ACCESS the cast exists to spell, and every caller so far is asking
|
|
634
|
+
* whether it may move the expression rather than how many accesses it holds. */
|
|
635
|
+
export function exprReadsVolatile(e: Expr, sfn: SFn): boolean {
|
|
636
|
+
const pointee = new Set(sfn.locals.filter((l) => l.pointeeVolatile).map((l) => l.name));
|
|
637
|
+
const object = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
|
|
638
|
+
const namesUnder = (x: Expr): string[] =>
|
|
639
|
+
[...subterms(x)].filter((y): y is Extract<Expr, { k: 'var' }> => y.k === 'var').map((y) => y.name);
|
|
640
|
+
return [...subterms(e)].some(
|
|
641
|
+
(x) =>
|
|
642
|
+
(x.k === 'cast' && x.volatile === true) ||
|
|
643
|
+
(x.k === 'var' && object.has(x.name)) ||
|
|
644
|
+
((x.k === 'index' || x.k === 'field') && namesUnder(x).some((n) => pointee.has(n))),
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** every node of an expression tree, itself included */
|
|
649
|
+
function* subterms(e: Expr): Generator<Expr> {
|
|
650
|
+
yield e;
|
|
651
|
+
for (const c of exprChildren(e)) {
|
|
652
|
+
yield* subterms(c);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
|
|
657
|
+
* splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
|
|
353
658
|
* init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
|
|
354
659
|
* sees the cond before the init. */
|
|
355
660
|
export function stmtChildren(s: Stmt): Stmt[] {
|
|
@@ -368,18 +673,117 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
368
673
|
return s.body;
|
|
369
674
|
case 'for':
|
|
370
675
|
return [s.init, s.inc, ...s.body];
|
|
676
|
+
case 'switch': {
|
|
677
|
+
const arms = s.cases.map((c) => c.body);
|
|
678
|
+
arms.splice(s.defaultAt ?? s.cases.length, 0, s.default ?? []);
|
|
679
|
+
return arms.flat();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** The nested statement LISTS of a statement — the SCOPES it opens.
|
|
685
|
+
*
|
|
686
|
+
* Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
|
|
687
|
+
* are single statements, not lists. A `for`'s body is the only list here, which is what a caller
|
|
688
|
+
* that PLACES into a list wants (`l3/scopebase.ts` — before the loop changes when a statement
|
|
689
|
+
* runs, inside the body repeats it) and not what a caller that reads the init as a DEF wants
|
|
690
|
+
* (`contracts.ts`'s dominance walk, which descends into the loop's parts itself).
|
|
691
|
+
*
|
|
692
|
+
* It lives here so a new `Stmt` kind carrying a list is one compile error rather than a silent
|
|
693
|
+
* miss in each caller's own recursion. Exhaustive on purpose: no `default`.
|
|
694
|
+
*/
|
|
695
|
+
export function stmtLists(s: Stmt): Stmt[][] {
|
|
696
|
+
switch (s.k) {
|
|
697
|
+
case 'if':
|
|
698
|
+
return [s.then, s.else];
|
|
699
|
+
case 'while':
|
|
700
|
+
case 'dowhile':
|
|
701
|
+
case 'for':
|
|
702
|
+
return [s.body];
|
|
703
|
+
case 'switch':
|
|
704
|
+
return [...s.cases.map((c) => c.body), ...(s.default ? [s.default] : [])];
|
|
705
|
+
case 'assign':
|
|
706
|
+
case 'store':
|
|
707
|
+
case 'exprstmt':
|
|
708
|
+
case 'return':
|
|
709
|
+
case 'break':
|
|
710
|
+
case 'continue':
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** The SETTER half of {@link stmtLists}: `s` with each of its nested lists replaced by `f` applied
|
|
716
|
+
* to it, in the same order `stmtLists` yields them. A caller that rebuilds a tree around one
|
|
717
|
+
* nested list therefore writes no `Stmt`-kind switch of its own — the pair lives here so a new
|
|
718
|
+
* kind carrying a list is a compile error in both halves rather than a silent miss in a caller's
|
|
719
|
+
* hand-rolled recursion. Exhaustive on purpose: no `default`. */
|
|
720
|
+
export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
|
|
721
|
+
switch (s.k) {
|
|
722
|
+
case 'if':
|
|
723
|
+
return { ...s, then: f(s.then), else: f(s.else) };
|
|
724
|
+
case 'while':
|
|
725
|
+
case 'dowhile':
|
|
726
|
+
case 'for':
|
|
727
|
+
return { ...s, body: f(s.body) };
|
|
371
728
|
case 'switch':
|
|
372
|
-
return
|
|
729
|
+
return {
|
|
730
|
+
...s,
|
|
731
|
+
cases: s.cases.map((c) => ({ ...c, body: f(c.body) })),
|
|
732
|
+
...(s.default ? { default: f(s.default) } : {}),
|
|
733
|
+
};
|
|
734
|
+
case 'assign':
|
|
735
|
+
case 'store':
|
|
736
|
+
case 'exprstmt':
|
|
737
|
+
case 'return':
|
|
738
|
+
case 'break':
|
|
739
|
+
case 'continue':
|
|
740
|
+
return s;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Every expression node in a body, statements nested and children included — the whole-tree walk
|
|
745
|
+
* `stmtChildren`, `stmtExprs` and `exprChildren` compose into, kept here so a new node kind is a
|
|
746
|
+
* compile error in one of them rather than a silent miss in each caller's own recursion.
|
|
747
|
+
*
|
|
748
|
+
* An EXPLICIT stack, not `yield*` recursion. A delegated generator costs a frame per nesting
|
|
749
|
+
* level on every value it forwards, so an expression ten levels down was handed off ten times; this
|
|
750
|
+
* walk runs over a whole function body once per emitted candidate. The stack
|
|
751
|
+
* holds either a statement list still to expand or an expression still to visit — `Expr` is
|
|
752
|
+
* never an array, so the two are told apart without a tag — and both are pushed in reverse so
|
|
753
|
+
* they pop in document order, which is the order the recursion produced. */
|
|
754
|
+
export function* walkExprs(body: Stmt[]): Generator<Expr> {
|
|
755
|
+
const stack: (Expr | Stmt[])[] = [body];
|
|
756
|
+
while (stack.length > 0) {
|
|
757
|
+
const top = stack.pop()!;
|
|
758
|
+
if (Array.isArray(top)) {
|
|
759
|
+
for (let i = top.length - 1; i >= 0; i--) {
|
|
760
|
+
const s = top[i];
|
|
761
|
+
const children = stmtChildren(s);
|
|
762
|
+
if (children.length > 0) {
|
|
763
|
+
stack.push(children);
|
|
764
|
+
}
|
|
765
|
+
const exprs = stmtExprs(s);
|
|
766
|
+
for (let j = exprs.length - 1; j >= 0; j--) {
|
|
767
|
+
stack.push(exprs[j]);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
yield top;
|
|
773
|
+
const children = exprChildren(top);
|
|
774
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
775
|
+
stack.push(children[i]);
|
|
776
|
+
}
|
|
373
777
|
}
|
|
374
778
|
}
|
|
375
779
|
|
|
376
780
|
// THE negation of a CONDITION — the one implementation, shared by every L3 pass that flips one.
|
|
377
781
|
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
782
|
+
// WHY IT IS SHARED RATHER THAN COPIED PER PASS: the passes that flip a condition do not run at one
|
|
783
|
+
// time. The structurer's empty-then peephole flips one, and `eliminateDeadStores` flips another
|
|
784
|
+
// AFTER structuring — so a copy that lacked a rule the other had would re-introduce the exact
|
|
785
|
+
// spelling the earlier pass just removed, and nothing downstream could tell that apart from a
|
|
786
|
+
// spelling the input really had. The negation therefore lives with the AST vocabulary.
|
|
383
787
|
//
|
|
384
788
|
// Three rules, in order:
|
|
385
789
|
// 1. a relational operator flips directly (`!=` → `==`, `<` → `>=`, …), exact over C's total
|
|
@@ -389,7 +793,7 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
389
793
|
// holds. Same operands, same inputs — which is what makes it safe over a `b` that loads. It
|
|
390
794
|
// matters because a source `&&` and its dual `||` compile to the SAME branch graph, so the
|
|
391
795
|
// recognizers in raise/shortcircuit.ts can only pick whichever the asm's branch senses spell;
|
|
392
|
-
// distributing is what lets the
|
|
796
|
+
// distributing is what lets the other one be spelled at all;
|
|
393
797
|
// 3. `!!x` collapses to `x`, reachable only from a double flip that rule 2 now produces.
|
|
394
798
|
//
|
|
395
799
|
// CONTEXT REQUIREMENT, and it is the reason this is `negateCond` and not `negate`: rule 3 is valid
|
|
@@ -397,13 +801,16 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
397
801
|
// so this must never be used to negate a general integer expression — only an `if`/loop test or an
|
|
398
802
|
// operand of one of the connectives above.
|
|
399
803
|
//
|
|
400
|
-
// SCOPE of rule 2: it
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
|
|
804
|
+
// SCOPE of rule 2: it fires wherever a branch-sense lever negates the condition, which is both `if`
|
|
805
|
+
// classes — `preserveDivergentBranchSense` on divergent ifs, `negateJoinedBranchSense` on
|
|
806
|
+
// reconverging ones — and on the joined class it is a DEFAULT emission, not a differ-only
|
|
807
|
+
// alternative, so a source `&&` can come out as its `||` dual with no lever asked for
|
|
808
|
+
// (`synthetic:ifand_far`, where the branch range put the fold on the other arm). Neither
|
|
809
|
+
// lever reaches a LOOP test, so a connective that ended up as one has no dual candidate at all —
|
|
810
|
+
// the differ never sees the other form, and on such a row this rule changes how the code READS and
|
|
811
|
+
// nothing else. Widening a branch-sense lever to loop tests is what would make it a matching lever
|
|
812
|
+
// there, and that is a separate change.
|
|
813
|
+
export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
|
|
407
814
|
'<': '>=',
|
|
408
815
|
'>=': '<',
|
|
409
816
|
'>': '<=',
|