@asmlift/core 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/l3/ast.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// asmlift L3 — the language-NEUTRAL structured AST. A LanguageBackend lowers this to a
|
|
2
2
|
// concrete language (C / Pascal / C++) and prints it. "Return a value" and binary ops
|
|
3
3
|
// are neutral nodes here; each backend owns its own spelling.
|
|
4
|
-
import type
|
|
4
|
+
import { type IrType, typeEquals } from '../ir/types';
|
|
5
5
|
|
|
6
6
|
export type Expr =
|
|
7
7
|
| { k: 'var'; name: string }
|
|
@@ -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,105 @@ 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 variation 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
|
+
// `baseAdvanced` is the THIRD, and it answers about the ADDRESS COMPUTATION: this access's
|
|
101
|
+
// address was reached by ADDING this many bytes to a register that already held — and had just
|
|
102
|
+
// been used as — another address (`adds r3, #2` between two stores), rather than by a second
|
|
103
|
+
// pool word or a memory-operand displacement. `raise/const.ts` records it on the literal its
|
|
104
|
+
// fold produces, because the fold is what makes the three shapes indistinguishable; the
|
|
105
|
+
// structure seam copies it here. `l3/advance.ts` is what reads it, to offer a pointer local
|
|
106
|
+
// advanced in place. `exprEquals` ignores it for `operandOff`'s reason. Absence is never proof:
|
|
107
|
+
// a target whose frontend does not lift the advance stamps nothing, and the value is a byte
|
|
108
|
+
// count that can be NEGATIVE, so readers test `!== undefined`.
|
|
109
|
+
| {
|
|
110
|
+
k: 'index';
|
|
111
|
+
base: Expr;
|
|
112
|
+
idx: Expr;
|
|
113
|
+
width: number;
|
|
114
|
+
signed: boolean;
|
|
115
|
+
lead?: Expr[];
|
|
116
|
+
/** The ELEMENT type the base's own DECLARATION gives it, for the one base whose stride the
|
|
117
|
+
* C type walk cannot reconstruct: a map-declared array MEMBER (`gPtr->arr`), which
|
|
118
|
+
* `exprCType` types `undefined` because the `field` node hangs off an untyped `var`. Without
|
|
119
|
+
* it the C backend legalizes the base through a reinterpret cast (`((u8 *)gPtr->arr)[i]`),
|
|
120
|
+
* which is the CAST form's object again and defeats the whole point of naming the member.
|
|
121
|
+
*
|
|
122
|
+
* IT IS A PRODUCER INVARIANT: the backend cannot vet a stated type. `derefStrideOk` tests
|
|
123
|
+
* the STATED type against the access width, never against the base, so it is true by
|
|
124
|
+
* construction for anything a producer could state AND for a statement gone stale. What
|
|
125
|
+
* the consumer does guard is PRECEDENCE — this field is consulted only where `exprCType`
|
|
126
|
+
* answers nothing (cfamily.ts `legalizedIndexBase`), so wherever the walk can read the base
|
|
127
|
+
* it corrects a stale statement instead of being overridden by it. The one producer,
|
|
128
|
+
* structure/structure.ts `pointeeElement`, sets it from the same `elemSize`/`elemSigned` it
|
|
129
|
+
* has just passed `spellsAccessType` on — and that predicate IS
|
|
130
|
+
* `typeEquals(T.int(width*8, elemSigned), scalarTypeForAccess(width, signed))`, so
|
|
131
|
+
* `derefStrideOk` over the stated pointer is true by construction at every width (4 by
|
|
132
|
+
* `width === 4`, 1 and 2 by `to.signed === signed`). The obligation is on the PRODUCER:
|
|
133
|
+
* state the type the access's own width and signedness agree with.
|
|
134
|
+
*
|
|
135
|
+
* THE ALTERNATIVE REJECTED: teach `exprCType` the pointee layout, the way
|
|
136
|
+
* `sym.noteGlobal` types the bare-array spelling —
|
|
137
|
+
* the printer already renders `u8 grid[6][8]` for this member from this same layout. It was
|
|
138
|
+
* rejected because `SFn.globals` is also the ADDRESSABLE-BASE list, so typing the symbol
|
|
139
|
+
* there admits it as a `/livebase` base (measured on the probe: 8 extra base locals). Typing
|
|
140
|
+
* the one node costs no candidates. */
|
|
141
|
+
baseElem?: IrType;
|
|
142
|
+
operandOff?: number;
|
|
143
|
+
baseOrdered?: true;
|
|
144
|
+
baseAdvanced?: number;
|
|
145
|
+
}
|
|
48
146
|
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
49
147
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
50
148
|
// Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
|
|
@@ -59,31 +157,41 @@ export type Expr =
|
|
|
59
157
|
// default) never produces this node; it keeps the `"?"` sentinel → ContractError behavior.
|
|
60
158
|
| { k: 'marker'; reason: string; args: Expr[] };
|
|
61
159
|
|
|
62
|
-
// `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
160
|
+
// THE SIGNEDNESS-CARRYING PAIRS. `>>` is the ARITHMETIC right shift and `>>>` the LOGICAL one;
|
|
161
|
+
// `/`/`%` are the SIGNED quotient and remainder and `/u`/`%u` the unsigned ones. C spells each pair
|
|
162
|
+
// with one token and picks between them from the operand types, so the C backend synthesizes the
|
|
163
|
+
// cast that pins the choice — exactly as it already synthesizes scalar deref casts from an `index`
|
|
164
|
+
// node's width. A backend with no spelling for one of them (IDO Pascal) declines LOUDLY on the
|
|
165
|
+
// operation itself, rather than on whatever artifact another language's spelling happened to leave
|
|
166
|
+
// in the tree.
|
|
167
|
+
//
|
|
168
|
+
// WHY THESE SPLITS AND NOT THE OTHERS. "The machine distinguishes them" is NOT the rule — the
|
|
169
|
+
// machine distinguishes `sltu`/`slt` too, and CMP_TO_BIN deliberately collapses `icmp_u*`→`<` etc.,
|
|
170
|
+
// noting that "unsignedness is in the operand types". Taking the machine as the rule would license
|
|
171
|
+
// more splits with no inhabitant, which is what "earn the level" forbids. The rule is the repo's
|
|
172
|
+
// own: a split is earned by a real, byte-load-bearing divergence WITH inhabitants that no other
|
|
173
|
+
// channel can carry. The shifts earned it first (~20 rows, 5 projects, 4 compilers) because the
|
|
174
|
+
// operand type could not carry them — a promoted narrow value is signed whatever it was loaded as.
|
|
67
175
|
//
|
|
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.
|
|
176
|
+
// The divides earned it second, on pokeemerald:GetAnchorCoord — `(u32)(coord * a1) / (u32)a0`
|
|
177
|
+
// standing beside two arithmetic shifts of the same values. Their only other channel is the operand
|
|
178
|
+
// TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
|
|
179
|
+
// once: the divisor also feeds a signed compare, so the /uns-cmp reconciliation correctly refuses
|
|
180
|
+
// it, and forcing it anyway makes agbcc delete the comparison as always-false. A per-operand pin is
|
|
181
|
+
// the only spelling that says "this division alone is unsigned".
|
|
76
182
|
//
|
|
77
|
-
// The
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
183
|
+
// The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
|
|
184
|
+
// which side a compare was spelled from genuinely underdetermines — a signed spelling that
|
|
185
|
+
// byte-matched was proved non-negative by the compiler — so it is refereed as a variation, while a
|
|
186
|
+
// division helper is a pure function of the expression's C type with no such proof available.
|
|
81
187
|
export type BinOp =
|
|
82
188
|
| '+'
|
|
83
189
|
| '-'
|
|
84
190
|
| '*'
|
|
85
191
|
| '/'
|
|
192
|
+
| '/u'
|
|
86
193
|
| '%'
|
|
194
|
+
| '%u'
|
|
87
195
|
| '<'
|
|
88
196
|
| '<='
|
|
89
197
|
| '>'
|
|
@@ -130,7 +238,20 @@ export type Stmt =
|
|
|
130
238
|
| { k: 'continue' }
|
|
131
239
|
// A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
|
|
132
240
|
// a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
|
|
133
|
-
// is emitted
|
|
241
|
+
// is emitted after `defaultAt` of them, or after all of them when that is absent.
|
|
242
|
+
//
|
|
243
|
+
// `defaultAt` exists because C lets `default:` sit BETWEEN case labels and a compiler that lays
|
|
244
|
+
// case bodies out in source order shows where the source put it. It is a COUNT of preceding arms,
|
|
245
|
+
// not an index into an array a later pass may rebuild, and a count past the arms is a producer bug
|
|
246
|
+
// a backend refuses. Setting it is legal only when the arm before the label does not fall through:
|
|
247
|
+
// moving the label in front of a falling arm would divert that arm into the default. The C-family
|
|
248
|
+
// printer terminates a non-final default with `break;` for the mirror-image reason.
|
|
249
|
+
//
|
|
250
|
+
// Unlike `fallsThrough` below, `defaultAt` is a SPELLING: the arm BEFORE the label is closed (the
|
|
251
|
+
// rule above) and so is the default body itself (the C-family printer terminates a non-final one),
|
|
252
|
+
// so a backend with no positional default (Pascal's `otherwise`) may ignore the count and still
|
|
253
|
+
// emit the same program. The arm AFTER the label is under no such rule and MAY fall through — it
|
|
254
|
+
// falls into the arm below it, which the label does not stand between.
|
|
134
255
|
//
|
|
135
256
|
// NON-NEUTRALITY NOTE (like the `index` node above): `fallsThrough` encodes a C/C++ control-flow
|
|
136
257
|
// concept POSITIONALLY — `cases[i].fallsThrough === true` means control continues into
|
|
@@ -139,7 +260,12 @@ export type Stmt =
|
|
|
139
260
|
// Pascal backend MUST loud-fail a `fallsThrough` case (it has no faithful spelling), exactly as it
|
|
140
261
|
// loud-fails `field`/`cast`. Recovery must therefore only set `fallsThrough` when the fall-through
|
|
141
262
|
// target is the emission-adjacent case.
|
|
142
|
-
|
|
263
|
+
//
|
|
264
|
+
// Recovery COMPUTES this flag rather than spelling it, so a source grep for `fallsThrough: true`
|
|
265
|
+
// finds hand-written fixtures and nothing else, whatever the corpus does — count its inhabitants
|
|
266
|
+
// by instrumenting the printer. Both regimes produce them: the jump table spells `case 4:` of
|
|
267
|
+
// `kleod:UpdateWorldMapNodeAnim`, the comparison tree `synthetic:sw_fallmem:agbcc`.
|
|
268
|
+
| { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[]; defaultAt?: number }
|
|
143
269
|
| { k: 'return'; value?: Expr };
|
|
144
270
|
|
|
145
271
|
/** One arm of a `switch`. `values` stacks multiple `case K:` labels onto one body (`case 1: case 2:`).
|
|
@@ -153,7 +279,59 @@ export interface SwitchCase {
|
|
|
153
279
|
export interface SFn {
|
|
154
280
|
name: string;
|
|
155
281
|
params: { name: string; type: IrType }[];
|
|
156
|
-
|
|
282
|
+
/** Recovered locals, declared at function top. Two INDEPENDENT volatility facts, mirroring
|
|
283
|
+
* symbols.ts's cell-vs-pointee split: `volatile` = the local OBJECT is volatile (the
|
|
284
|
+
* address-escaped frame scratch; dce.ts treats reads of it as observable), `pointeeVolatile`
|
|
285
|
+
* = the local is a pointer TO volatile data (the l3/volatileptr.ts variation; a declaration
|
|
286
|
+
* spelling only — nothing about the local itself is observable).
|
|
287
|
+
*
|
|
288
|
+
* `frame` is present on a local the structurer recovered from an `laddr` — the asm
|
|
289
|
+
* MATERIALIZED the slot's address into a register, so the object provably lives in memory —
|
|
290
|
+
* and carries the machine's static access counts for it. Under Thumb that envelope is a
|
|
291
|
+
* SUB-WORD frame object: `strh/ldrh/strb/ldrb` have no `[sp,#imm]` form, so a compiler must
|
|
292
|
+
* copy `sp` first, while a word spill goes straight to `[sp,#imm]` and is recovered as an
|
|
293
|
+
* SSA value with no local of its own. So `frame` is NOT the set of every value the machine
|
|
294
|
+
* slotted. `loads`/`stores` are the yardstick a qualifier variation must match before it may
|
|
295
|
+
* declare every access to the object observable: the readability passes between here and L3
|
|
296
|
+
* may drop a store or render one machine load as two reads, and `volatile` over an access
|
|
297
|
+
* set asmlift did not preserve is a source that contradicts itself. ABSENT where the counts
|
|
298
|
+
* would be a floor rather than the set: an address reaching anything but a direct load/store
|
|
299
|
+
* leaves accesses the count cannot see.
|
|
300
|
+
*
|
|
301
|
+
* ORDER MATTERS, and it means two different things at two different times. As the structurer
|
|
302
|
+
* builds it and as every L3 pass sees it, this is the RECOVERED DECLARATION ORDER — the naming
|
|
303
|
+
* walk's order — and passes reason about it as such: `l3/coalesce.ts` picks the arm-disjoint
|
|
304
|
+
* survivor by position in THIS list, so the earlier declaration wins the way a shared source
|
|
305
|
+
* local reads. The EMITTED order is this list re-sorted by `l3/slotorder.ts` inside `emit`,
|
|
306
|
+
* which happens after every pass and returns a copy. A pass that sorted the list any earlier
|
|
307
|
+
* would silently change which local survives every arm-disjoint merge.
|
|
308
|
+
*
|
|
309
|
+
* ONE COMMENT, DELIBERATELY. Five sites tell a reader to consult "the SFn.locals doc"
|
|
310
|
+
* (structure/structure.ts, l3/inlinebase.ts, l3/volatileval.ts, l3/reindex.ts, l3/unreduce.ts),
|
|
311
|
+
* and TypeScript attaches only the LAST doc block before a declaration — so a second block
|
|
312
|
+
* added in front of this one would silently orphan everything above and those five references
|
|
313
|
+
* would point at half a paragraph. Append here; do not add a neighbour. */
|
|
314
|
+
locals: {
|
|
315
|
+
name: string;
|
|
316
|
+
type: IrType;
|
|
317
|
+
volatile?: true;
|
|
318
|
+
pointeeVolatile?: true;
|
|
319
|
+
frame?: { loads: number; stores: number };
|
|
320
|
+
/** the local stands on an `undef` — storage the asm reads without ever writing it, where the
|
|
321
|
+
* MISSING assignment is the recovery. Marked because a local read and never assigned is
|
|
322
|
+
* otherwise a dropped statement (contracts.ts assertLocalsWritten). */
|
|
323
|
+
uninit?: true;
|
|
324
|
+
/** every `[sp,#k]` the machine homed this local at, when the asm spilled it — ascending, and
|
|
325
|
+
* usually one. Several when the naming walk put several spilled values under this one name,
|
|
326
|
+
* or a coalesce absorbed a second homed local into it; the list is the UNION and picks
|
|
327
|
+
* nothing, because which offset is the earlier DECLARATION RANK depends on the frame's
|
|
328
|
+
* direction and only `l3/slotorder.ts` holds it (ir/core.ts `SlotHomes`).
|
|
329
|
+
*
|
|
330
|
+
* Present ONLY on a local recovered from word-spill VALUES. A `frame` local and a `uninit`
|
|
331
|
+
* one are deliberately left unstamped even where the offset is in hand — see the refusal at
|
|
332
|
+
* the structurer's build site for the measurement that decided it. */
|
|
333
|
+
slots?: number[];
|
|
334
|
+
}[];
|
|
157
335
|
/** project globals referenced with a known declaration shape (symbol map) — typed for the
|
|
158
336
|
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
159
337
|
* declare them, exactly like every other global name asmlift emits. */
|
|
@@ -163,6 +341,18 @@ export interface SFn {
|
|
|
163
341
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
164
342
|
* unless raise/structs.ts recovered a struct. Sorted by name for deterministic output. */
|
|
165
343
|
structs?: StructType[];
|
|
344
|
+
/** Which way this compiler hands out frame slots against DECLARATION RANK, when it is known:
|
|
345
|
+
*
|
|
346
|
+
* THE ONE COMPILER DATUM ON THE NEUTRAL TREE, and it is here rather than in a backend on
|
|
347
|
+
* purpose: `LanguageBackend.emit(fn: SFn): string` is the only seam a backend has, and widening
|
|
348
|
+
* it to `emit(fn, opts)` would hand the datum back to the seven `.emit(` CALL SITES this design
|
|
349
|
+
* exists to keep it out of. So the tree is neutral in its NODES — every node still spells the
|
|
350
|
+
* same thing in C, C++ and Pascal — and not in its emission policy, which this field is.
|
|
351
|
+
* `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Set by the
|
|
352
|
+
* structurer from `StructureOptions.spillSlotOrder`, itself a compiler behavior declared in
|
|
353
|
+
* `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
|
|
354
|
+
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
355
|
+
slotOrder?: 'ascending' | 'descending';
|
|
166
356
|
}
|
|
167
357
|
|
|
168
358
|
/** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
|
|
@@ -177,6 +367,14 @@ export interface StructType {
|
|
|
177
367
|
* comment spelling. */
|
|
178
368
|
export interface LanguageBackend {
|
|
179
369
|
readonly id: 'c' | 'cpp' | 'pascal';
|
|
370
|
+
/** Can this language spell a `switch` arm that RUNS ON into the next one (`fallsThrough`)?
|
|
371
|
+
* C and C++ can; Pascal's `case-of` cannot, and its backend loud-fails the node (see the
|
|
372
|
+
* non-neutrality note on `Stmt`'s `switch`). Declared here rather than inferred from `id`
|
|
373
|
+
* because it is what RECOVERY must ask: a comparison-tree switch has a second, behaviourally
|
|
374
|
+
* identical recovery (plain if-nesting), so minting a `fallsThrough` arm for a backend that
|
|
375
|
+
* cannot print it turns a whole function that used to decompile into a loud stub. The
|
|
376
|
+
* structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
|
|
377
|
+
readonly spellsSwitchFallthrough: boolean;
|
|
180
378
|
emit(fn: SFn): string;
|
|
181
379
|
// Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
|
|
182
380
|
// Used by the annotate-mode stub path to carry the failure reason + the original asm
|
|
@@ -201,8 +399,17 @@ export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
|
|
|
201
399
|
}
|
|
202
400
|
|
|
203
401
|
/** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
|
|
204
|
-
* fieldSpellsDot/derefStrideOk)
|
|
205
|
-
*
|
|
402
|
+
* fieldSpellsDot/derefStrideOk), exhaustive under noImplicitReturns like the walkers below.
|
|
403
|
+
*
|
|
404
|
+
* Key-order-independent for every EXPR field — a switch, not a stringify. NOT for the `cast`
|
|
405
|
+
* arm's TARGET TYPE, which is compared by serialization and so is order-sensitive: two `IrType`
|
|
406
|
+
* objects with the same fields written in a different order compare UNEQUAL. That is why the
|
|
407
|
+
* inline `IrType` literals elsewhere in the codebase are written in `T.int`/`T.ptr` key order —
|
|
408
|
+
* matching the constructors keeps them comparable against constructed types.
|
|
409
|
+
*
|
|
410
|
+
* `typeEquals` (ir/types.ts) is NOT the fix: it ignores `struct.size`, so it is strictly more
|
|
411
|
+
* permissive, and swapping it in here would let a CSE collapse two accesses whose struct stride
|
|
412
|
+
* differs. */
|
|
206
413
|
export function exprEquals(a: Expr, b: Expr): boolean {
|
|
207
414
|
if (a.k !== b.k) {
|
|
208
415
|
return false;
|
|
@@ -224,7 +431,14 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
224
431
|
}
|
|
225
432
|
case 'cast': {
|
|
226
433
|
const bb = b as typeof a;
|
|
227
|
-
|
|
434
|
+
// `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
|
|
435
|
+
// CSE or dedup that treats these as equal keeps one node and drops the other, silently
|
|
436
|
+
// rewriting a volatile access as a plain one.
|
|
437
|
+
return (
|
|
438
|
+
JSON.stringify(a.to) === JSON.stringify(bb.to) &&
|
|
439
|
+
(a.volatile ?? false) === (bb.volatile ?? false) &&
|
|
440
|
+
exprEquals(a.e, bb.e)
|
|
441
|
+
);
|
|
228
442
|
}
|
|
229
443
|
case 'call': {
|
|
230
444
|
const bb = b as typeof a;
|
|
@@ -234,13 +448,23 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
234
448
|
const bb = b as typeof a;
|
|
235
449
|
// `lead` is part of the ADDRESS (`g[0][i]` and `g[1][i]` are different elements), so it
|
|
236
450
|
// must be compared — an omission here would let CSE/dedup collapse two distinct accesses.
|
|
451
|
+
// The EVIDENCE fields (`operandOff`, `baseOrdered`, `baseAdvanced`) deliberately are NOT:
|
|
452
|
+
// two accesses agreeing on everything else denote the same cell and print the same subscript
|
|
453
|
+
// however the machine spelled the offset, so a CSE that collapses them respells nothing.
|
|
237
454
|
const lead = a.lead ?? [];
|
|
238
455
|
const bLead = bb.lead ?? [];
|
|
239
456
|
return (
|
|
240
457
|
a.width === bb.width &&
|
|
241
458
|
a.signed === bb.signed &&
|
|
242
459
|
lead.length === bLead.length &&
|
|
243
|
-
lead.every((v, i) => v
|
|
460
|
+
lead.every((v, i) => exprEquals(v, bLead[i])) &&
|
|
461
|
+
// `baseElem` is part of the SPELLING for the same reason `lead` is: it decides whether the
|
|
462
|
+
// base takes the reinterpret cast, so two otherwise-equal nodes disagreeing about it print
|
|
463
|
+
// two different expressions. (Its one producer derives it from the base and the width, so
|
|
464
|
+
// nodes that agree on those agree here too — this cannot reject a real CSE.)
|
|
465
|
+
(a.baseElem === undefined
|
|
466
|
+
? bb.baseElem === undefined
|
|
467
|
+
: bb.baseElem !== undefined && typeEquals(a.baseElem, bb.baseElem)) &&
|
|
244
468
|
exprEquals(a.base, bb.base) &&
|
|
245
469
|
exprEquals(a.idx, bb.idx)
|
|
246
470
|
);
|
|
@@ -248,7 +472,7 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
248
472
|
case 'field': {
|
|
249
473
|
const bb = b as typeof a;
|
|
250
474
|
// `dot` is part of the SPELLING, and for the same reason `lead` is compared above: a CSE or
|
|
251
|
-
// dedup that treats these as equal keeps one node and discards the other, silently
|
|
475
|
+
// dedup that treats these as equal keeps one node and discards the other, silently rewriting
|
|
252
476
|
// `p->field_4` as `p.field_4` (or the reverse). Both compile only for the base type each
|
|
253
477
|
// belongs to, so collapsing them is how a valid access becomes an invalid one — or worse, a
|
|
254
478
|
// valid one against a different object.
|
|
@@ -263,14 +487,6 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
263
487
|
}
|
|
264
488
|
}
|
|
265
489
|
|
|
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
490
|
/** THE spelling of an unmodelled instruction's gap reason, in one place: `structure.ts` writes it
|
|
275
491
|
* into the marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
|
|
276
492
|
* classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
|
|
@@ -279,6 +495,14 @@ export function gapReasonFor(mnemonic: unknown): string {
|
|
|
279
495
|
return `unmodelled instruction '${typeof mnemonic === 'string' ? mnemonic : '?'}'`;
|
|
280
496
|
}
|
|
281
497
|
|
|
498
|
+
// ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
|
|
499
|
+
// Every generic walker derives from these helpers, so a NEW node kind is a compile error in
|
|
500
|
+
// exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
|
|
501
|
+
// hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
|
|
502
|
+
// SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
|
|
503
|
+
// recognizeForLoops) rightly keep their own switches.
|
|
504
|
+
|
|
505
|
+
/** The direct sub-expressions of `e`, in syntactic order. */
|
|
282
506
|
export function exprChildren(e: Expr): Expr[] {
|
|
283
507
|
switch (e.k) {
|
|
284
508
|
case 'var':
|
|
@@ -293,7 +517,7 @@ export function exprChildren(e: Expr): Expr[] {
|
|
|
293
517
|
case 'call':
|
|
294
518
|
return e.args;
|
|
295
519
|
case 'index':
|
|
296
|
-
return [e.base, e.idx];
|
|
520
|
+
return [e.base, ...(e.lead ?? []), e.idx];
|
|
297
521
|
case 'field':
|
|
298
522
|
return [e.base];
|
|
299
523
|
case 'marker':
|
|
@@ -316,7 +540,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
|
|
|
316
540
|
case 'call':
|
|
317
541
|
return { ...e, args: e.args.map(f) };
|
|
318
542
|
case 'index':
|
|
319
|
-
return { ...e, base: f(e.base), idx: f(e.idx) };
|
|
543
|
+
return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
|
|
320
544
|
case 'field':
|
|
321
545
|
return { ...e, base: f(e.base) };
|
|
322
546
|
case 'marker':
|
|
@@ -349,7 +573,132 @@ export function stmtExprs(s: Stmt): Expr[] {
|
|
|
349
573
|
}
|
|
350
574
|
}
|
|
351
575
|
|
|
352
|
-
/**
|
|
576
|
+
/** Rebuild `s` with EVERY expression position mapped through `f` — store lvalues and loop/switch
|
|
577
|
+
* heads included, nested statements recursively. The rewrite dual of stmtExprs/stmtChildren, for
|
|
578
|
+
* the PURE 1:1 case: one expression in, one expression out, every statement kept.
|
|
579
|
+
*
|
|
580
|
+
* A PASS THAT HAND-ROLLS ITS OWN MAPPER IS NOT A MISSED MIGRATION. Several do, because their
|
|
581
|
+
* contract is not this one — a rewrite that may DECLINE, one that INSERTS statements, one that
|
|
582
|
+
* rewrites assign TARGETS as well as expressions, one that turns a statement into a list. Check
|
|
583
|
+
* the contract before pointing one of them here. */
|
|
584
|
+
export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
|
|
585
|
+
const mapS = (x: Stmt): Stmt => mapStmtExprs(x, f);
|
|
586
|
+
switch (s.k) {
|
|
587
|
+
case 'assign':
|
|
588
|
+
return { ...s, value: f(s.value) };
|
|
589
|
+
case 'store':
|
|
590
|
+
return { ...s, lval: f(s.lval), value: f(s.value) };
|
|
591
|
+
case 'exprstmt':
|
|
592
|
+
return { ...s, value: f(s.value) };
|
|
593
|
+
case 'return':
|
|
594
|
+
return s.value ? { ...s, value: f(s.value) } : s;
|
|
595
|
+
case 'if':
|
|
596
|
+
return { ...s, cond: f(s.cond), then: s.then.map(mapS), else: s.else.map(mapS) };
|
|
597
|
+
case 'while':
|
|
598
|
+
case 'dowhile':
|
|
599
|
+
return { ...s, cond: f(s.cond), body: s.body.map(mapS) };
|
|
600
|
+
case 'for':
|
|
601
|
+
return { ...s, init: mapS(s.init), cond: f(s.cond), inc: mapS(s.inc), body: s.body.map(mapS) };
|
|
602
|
+
case 'switch':
|
|
603
|
+
return {
|
|
604
|
+
...s,
|
|
605
|
+
scrutinee: f(s.scrutinee),
|
|
606
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(mapS) })),
|
|
607
|
+
default: s.default?.map(mapS),
|
|
608
|
+
};
|
|
609
|
+
case 'break':
|
|
610
|
+
case 'continue':
|
|
611
|
+
return s;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** An address the target can REMATERIALIZE: a constant expression, reading no variable and no
|
|
616
|
+
* memory. Which ENCODING the compiler picked for it is not a property of the source — agbcc
|
|
617
|
+
* spells a pool word `(s32 *)33569456` but a shift-encodable one `(s32 *)(128 << 18)`, and every
|
|
618
|
+
* GBA hardware region (EWRAM 0x2000000, I/O 0x4000000, VRAM 0x6000000 …) takes the second form —
|
|
619
|
+
* so both must reach the same admission or the whole MMIO/VRAM fill family declines on its
|
|
620
|
+
* address. A bare `(T *)0` is excluded — a null base is not a walk — but the test is on the
|
|
621
|
+
* LITERALS the expression mentions, not on the value they fold to, so `(T *)(5 - 5)` passes.
|
|
622
|
+
* Folding would need a constant evaluator no consumer has another use for, and both consumers
|
|
623
|
+
* keep the expression verbatim, so no decision downstream reads the value.
|
|
624
|
+
*
|
|
625
|
+
* Two ask it: the walk re-index (l3/reindex.ts) about a walk base, and the `volatile` qualifier
|
|
626
|
+
* (l3/volatileptr.ts) about what feeds a pointer local. They must agree — a MMIO fill whose base
|
|
627
|
+
* one admits and the other refuses can be re-indexed but never qualified, so the paired
|
|
628
|
+
* `/indexed/volatile` spelling is unreachable at exactly the hardware addresses it is for.
|
|
629
|
+
*
|
|
630
|
+
* Two respell variations reading the same initializers are deliberately NOT here: l3/inlinebase.ts
|
|
631
|
+
* substitutes the address at each use, l3/nearbase.ts clusters neighbours by distance, and both
|
|
632
|
+
* need the VALUE, which is the evaluator above. Declining a shift-encoded base there costs a
|
|
633
|
+
* variation that does not fire, and the population is small: over klonoa's 531 lifting functions,
|
|
634
|
+
* one inlinebase-shaped local with no symbol map and none with it; a folded nearbase would form
|
|
635
|
+
* a new cluster in 4 functions mapless and 1 with the map. Both counts were zero over the agbcc
|
|
636
|
+
* benchmark rows that lifted when the variations landed — a MEASUREMENT over a corpus that grows, so
|
|
637
|
+
* re-take it rather than quoting it: a new row falsifies the number, not the argument. */
|
|
638
|
+
export function rematerializableAddress(e: Expr): boolean {
|
|
639
|
+
let nonZero = false;
|
|
640
|
+
let ok = true;
|
|
641
|
+
const visit = (x: Expr): void => {
|
|
642
|
+
switch (x.k) {
|
|
643
|
+
case 'const':
|
|
644
|
+
nonZero ||= x.value !== 0;
|
|
645
|
+
break;
|
|
646
|
+
case 'cast':
|
|
647
|
+
case 'bin':
|
|
648
|
+
case 'un':
|
|
649
|
+
break;
|
|
650
|
+
default:
|
|
651
|
+
ok = false; // var, addr, index, field, call, marker
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
for (const c of exprChildren(x)) {
|
|
655
|
+
visit(c);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
visit(e);
|
|
659
|
+
return ok && nonZero;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, or a marker
|
|
663
|
+
* standing in for an unmodelled instruction (annotate mode). */
|
|
664
|
+
export function exprHasEffect(e: Expr): boolean {
|
|
665
|
+
return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
|
|
669
|
+
* move, and the one `exprHasEffect` deliberately does not answer: that one is about a call, this
|
|
670
|
+
* about a QUALIFIER, and a pass that asks the first where it means the second reorders device
|
|
671
|
+
* accesses while its gate reports clean.
|
|
672
|
+
*
|
|
673
|
+
* Three spellings assert one thing, and all three are here because a pass that knew only the cast
|
|
674
|
+
* would miss the two a later variation writes: a `volatile` cast (where a raw address carries it), a
|
|
675
|
+
* read through a pointer local declared to point at volatile data (l3/volatileptr.ts), and a read
|
|
676
|
+
* of a `volatile` local object (l3/volatileval.ts). A bare cast counts even with no deref under it
|
|
677
|
+
* — the qualifier is on the ACCESS the cast exists to spell, and every caller so far is asking
|
|
678
|
+
* whether it may move the expression rather than how many accesses it holds. */
|
|
679
|
+
export function exprReadsVolatile(e: Expr, sfn: SFn): boolean {
|
|
680
|
+
const pointee = new Set(sfn.locals.filter((l) => l.pointeeVolatile).map((l) => l.name));
|
|
681
|
+
const object = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
|
|
682
|
+
const namesUnder = (x: Expr): string[] =>
|
|
683
|
+
[...subterms(x)].filter((y): y is Extract<Expr, { k: 'var' }> => y.k === 'var').map((y) => y.name);
|
|
684
|
+
return [...subterms(e)].some(
|
|
685
|
+
(x) =>
|
|
686
|
+
(x.k === 'cast' && x.volatile === true) ||
|
|
687
|
+
(x.k === 'var' && object.has(x.name)) ||
|
|
688
|
+
((x.k === 'index' || x.k === 'field') && namesUnder(x).some((n) => pointee.has(n))),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** every node of an expression tree, itself included */
|
|
693
|
+
function* subterms(e: Expr): Generator<Expr> {
|
|
694
|
+
yield e;
|
|
695
|
+
for (const c of exprChildren(e)) {
|
|
696
|
+
yield* subterms(c);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
|
|
701
|
+
* splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
|
|
353
702
|
* init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
|
|
354
703
|
* sees the cond before the init. */
|
|
355
704
|
export function stmtChildren(s: Stmt): Stmt[] {
|
|
@@ -368,18 +717,117 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
368
717
|
return s.body;
|
|
369
718
|
case 'for':
|
|
370
719
|
return [s.init, s.inc, ...s.body];
|
|
720
|
+
case 'switch': {
|
|
721
|
+
const arms = s.cases.map((c) => c.body);
|
|
722
|
+
arms.splice(s.defaultAt ?? s.cases.length, 0, s.default ?? []);
|
|
723
|
+
return arms.flat();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** The nested statement LISTS of a statement — the SCOPES it opens.
|
|
729
|
+
*
|
|
730
|
+
* Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
|
|
731
|
+
* are single statements, not lists. A `for`'s body is the only list here, which is what a caller
|
|
732
|
+
* that PLACES into a list wants (`l3/scopebase.ts` — before the loop changes when a statement
|
|
733
|
+
* runs, inside the body repeats it) and not what a caller that reads the init as a DEF wants
|
|
734
|
+
* (`contracts.ts`'s dominance walk, which descends into the loop's parts itself).
|
|
735
|
+
*
|
|
736
|
+
* It lives here so a new `Stmt` kind carrying a list is one compile error rather than a silent
|
|
737
|
+
* miss in each caller's own recursion. Exhaustive on purpose: no `default`.
|
|
738
|
+
*/
|
|
739
|
+
export function stmtLists(s: Stmt): Stmt[][] {
|
|
740
|
+
switch (s.k) {
|
|
741
|
+
case 'if':
|
|
742
|
+
return [s.then, s.else];
|
|
743
|
+
case 'while':
|
|
744
|
+
case 'dowhile':
|
|
745
|
+
case 'for':
|
|
746
|
+
return [s.body];
|
|
747
|
+
case 'switch':
|
|
748
|
+
return [...s.cases.map((c) => c.body), ...(s.default ? [s.default] : [])];
|
|
749
|
+
case 'assign':
|
|
750
|
+
case 'store':
|
|
751
|
+
case 'exprstmt':
|
|
752
|
+
case 'return':
|
|
753
|
+
case 'break':
|
|
754
|
+
case 'continue':
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** The SETTER half of {@link stmtLists}: `s` with each of its nested lists replaced by `f` applied
|
|
760
|
+
* to it, in the same order `stmtLists` yields them. A caller that rebuilds a tree around one
|
|
761
|
+
* nested list therefore writes no `Stmt`-kind switch of its own — the pair lives here so a new
|
|
762
|
+
* kind carrying a list is a compile error in both halves rather than a silent miss in a caller's
|
|
763
|
+
* hand-rolled recursion. Exhaustive on purpose: no `default`. */
|
|
764
|
+
export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
|
|
765
|
+
switch (s.k) {
|
|
766
|
+
case 'if':
|
|
767
|
+
return { ...s, then: f(s.then), else: f(s.else) };
|
|
768
|
+
case 'while':
|
|
769
|
+
case 'dowhile':
|
|
770
|
+
case 'for':
|
|
771
|
+
return { ...s, body: f(s.body) };
|
|
371
772
|
case 'switch':
|
|
372
|
-
return
|
|
773
|
+
return {
|
|
774
|
+
...s,
|
|
775
|
+
cases: s.cases.map((c) => ({ ...c, body: f(c.body) })),
|
|
776
|
+
...(s.default ? { default: f(s.default) } : {}),
|
|
777
|
+
};
|
|
778
|
+
case 'assign':
|
|
779
|
+
case 'store':
|
|
780
|
+
case 'exprstmt':
|
|
781
|
+
case 'return':
|
|
782
|
+
case 'break':
|
|
783
|
+
case 'continue':
|
|
784
|
+
return s;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Every expression node in a body, statements nested and children included — the whole-tree walk
|
|
789
|
+
* `stmtChildren`, `stmtExprs` and `exprChildren` compose into, kept here so a new node kind is a
|
|
790
|
+
* compile error in one of them rather than a silent miss in each caller's own recursion.
|
|
791
|
+
*
|
|
792
|
+
* An EXPLICIT stack, not `yield*` recursion. A delegated generator costs a frame per nesting
|
|
793
|
+
* level on every value it forwards, so an expression ten levels down was handed off ten times; this
|
|
794
|
+
* walk runs over a whole function body once per emitted candidate. The stack
|
|
795
|
+
* holds either a statement list still to expand or an expression still to visit — `Expr` is
|
|
796
|
+
* never an array, so the two are told apart without a tag — and both are pushed in reverse so
|
|
797
|
+
* they pop in document order, which is the order the recursion produced. */
|
|
798
|
+
export function* walkExprs(body: Stmt[]): Generator<Expr> {
|
|
799
|
+
const stack: (Expr | Stmt[])[] = [body];
|
|
800
|
+
while (stack.length > 0) {
|
|
801
|
+
const top = stack.pop()!;
|
|
802
|
+
if (Array.isArray(top)) {
|
|
803
|
+
for (let i = top.length - 1; i >= 0; i--) {
|
|
804
|
+
const s = top[i];
|
|
805
|
+
const children = stmtChildren(s);
|
|
806
|
+
if (children.length > 0) {
|
|
807
|
+
stack.push(children);
|
|
808
|
+
}
|
|
809
|
+
const exprs = stmtExprs(s);
|
|
810
|
+
for (let j = exprs.length - 1; j >= 0; j--) {
|
|
811
|
+
stack.push(exprs[j]);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
yield top;
|
|
817
|
+
const children = exprChildren(top);
|
|
818
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
819
|
+
stack.push(children[i]);
|
|
820
|
+
}
|
|
373
821
|
}
|
|
374
822
|
}
|
|
375
823
|
|
|
376
824
|
// THE negation of a CONDITION — the one implementation, shared by every L3 pass that flips one.
|
|
377
825
|
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
826
|
+
// WHY IT IS SHARED RATHER THAN COPIED PER PASS: the passes that flip a condition do not run at one
|
|
827
|
+
// time. The structurer's empty-then peephole flips one, and `eliminateDeadStores` flips another
|
|
828
|
+
// AFTER structuring — so a copy that lacked a rule the other had would re-introduce the exact
|
|
829
|
+
// spelling the earlier pass just removed, and nothing downstream could tell that apart from a
|
|
830
|
+
// spelling the input really had. The negation therefore lives with the AST vocabulary.
|
|
383
831
|
//
|
|
384
832
|
// Three rules, in order:
|
|
385
833
|
// 1. a relational operator flips directly (`!=` → `==`, `<` → `>=`, …), exact over C's total
|
|
@@ -389,7 +837,7 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
389
837
|
// holds. Same operands, same inputs — which is what makes it safe over a `b` that loads. It
|
|
390
838
|
// matters because a source `&&` and its dual `||` compile to the SAME branch graph, so the
|
|
391
839
|
// recognizers in raise/shortcircuit.ts can only pick whichever the asm's branch senses spell;
|
|
392
|
-
// distributing is what lets the
|
|
840
|
+
// distributing is what lets the other one be spelled at all;
|
|
393
841
|
// 3. `!!x` collapses to `x`, reachable only from a double flip that rule 2 now produces.
|
|
394
842
|
//
|
|
395
843
|
// CONTEXT REQUIREMENT, and it is the reason this is `negateCond` and not `negate`: rule 3 is valid
|
|
@@ -397,13 +845,16 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
397
845
|
// so this must never be used to negate a general integer expression — only an `if`/loop test or an
|
|
398
846
|
// operand of one of the connectives above.
|
|
399
847
|
//
|
|
400
|
-
// SCOPE of rule 2: it
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
|
|
848
|
+
// SCOPE of rule 2: it fires wherever a branch-sense variation negates the condition, which is both `if`
|
|
849
|
+
// classes — `preserveDivergentBranchSense` on divergent ifs, `negateJoinedBranchSense` on
|
|
850
|
+
// reconverging ones — and on the joined class it is a DEFAULT emission, not a differ-only
|
|
851
|
+
// alternative, so a source `&&` can come out as its `||` dual with no variation asked for
|
|
852
|
+
// (`synthetic:ifand_far`, where the branch range put the fold on the other arm). Neither
|
|
853
|
+
// variation reaches a LOOP test, so a connective that ended up as one has no dual candidate at all —
|
|
854
|
+
// the differ never sees the other form, and on such a row this rule changes how the code READS and
|
|
855
|
+
// nothing else. Widening a branch-sense variation to loop tests is what would make it a matching variation
|
|
856
|
+
// there, and that is a separate change.
|
|
857
|
+
export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
|
|
407
858
|
'<': '>=',
|
|
408
859
|
'>=': '<',
|
|
409
860
|
'>': '<=',
|