@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
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
// L3 respell variation: UN-REDUCE a loop-carried accumulator — delete `v = INIT; … v = v + K;`
|
|
2
|
+
// and spell each read as the closed form `INIT[start := counter]`.
|
|
3
|
+
//
|
|
4
|
+
// v0 = (a0 << 6) + a1; while (v1 <= 31) {
|
|
5
|
+
// while (v1 <= 31) { ⇒ *(s32 *)REG = (v1 << 6) + a1;
|
|
6
|
+
// *(s32 *)REG = v0; v1 = v1 + 1;
|
|
7
|
+
// v0 = v0 + 64; v1 = v1 + 1; } }
|
|
8
|
+
//
|
|
9
|
+
// WHY IT IS A SPELLING AND NOT A FIX. Strength reduction is a compiler pass, so the accumulated
|
|
10
|
+
// form is what the ASM shows whichever form the source had — a source `a[i]` and a source
|
|
11
|
+
// `p = a; … p++` compile to the same induction variable. asmlift recovers the reduced form
|
|
12
|
+
// because that is what the machine ran; the un-reduced form is the other pre-image, and the differ
|
|
13
|
+
// referees. (l3/reindex.ts makes the same argument for a POINTER WALK; this is its scalar-value
|
|
14
|
+
// sibling, and the two do not overlap — `reindexWalks` refuses a function with no pointer local or
|
|
15
|
+
// param.)
|
|
16
|
+
//
|
|
17
|
+
// WHAT IT BUYS, and it is not readability. A compiler-created giv init is emitted by
|
|
18
|
+
// `emit_iv_add_mult` at `loop_start` (gcc/loop.c:4761, inserted at :6985) — during
|
|
19
|
+
// `strength_reduce`, which agbcc runs AFTER `move_movables` hoists the loop invariants
|
|
20
|
+
// (gcc/loop.c:1151 then :1173). Both insert immediately before the loop, so the giv's init lands
|
|
21
|
+
// BELOW everything the invariant hoist put there. No C statement can reach that slot: statement
|
|
22
|
+
// order forces a user-written assignment above the whole preheader. Measured on
|
|
23
|
+
// synthetic:dmafill, holding the rest fixed — a plain statement before the loop scores 19, the
|
|
24
|
+
// same statement under an explicit guard 15, and letting the compiler create the giv 0.
|
|
25
|
+
//
|
|
26
|
+
// ONE RELATION, TWO SPELLINGS — and getting that the right way round is what keeps a third
|
|
27
|
+
// spelling from becoming a third function. From the driver's two facts alone — `acc` starts at
|
|
28
|
+
// `INIT` and is stepped by the constant `k`; `ctr` starts at `start` and is stepped by the
|
|
29
|
+
// constant `d` — the identity
|
|
30
|
+
//
|
|
31
|
+
// acc(ctr) = INIT + (ctr - start) * (k / d)
|
|
32
|
+
//
|
|
33
|
+
// follows, and it says NOTHING about the init's shape. `rec` is not a second relation: it is a
|
|
34
|
+
// prettier spelling of that same value, and its `+`-spine walk exists precisely to prove that
|
|
35
|
+
// `INIT[start := ctr]` equals it. `relateFolded` is the identity's degenerate corner written out.
|
|
36
|
+
//
|
|
37
|
+
// WHICH SPELLING APPLIES IS DECIDED BY CONSTANT FOLDING. The substitution needs the counter's
|
|
38
|
+
// start to still be present in the init. A source `for (i = 0; …) use(base + (i << 6))` gives the
|
|
39
|
+
// giv the init `base + (0 << 6)`, and the compiler folds that to `base` long before any of it
|
|
40
|
+
// reaches the asm — the start term is GONE and there is nothing to substitute for. Every corpus
|
|
41
|
+
// inhabitant of the substitutional spelling starts its counter at a PARAMETER (`dmafill`'s `lo`),
|
|
42
|
+
// which is exactly why none of them needed the other: a symbolic start cannot fold.
|
|
43
|
+
// `synthetic:offloop`, `offgiv`, `offgiv2` and `offgiv3` are the shape that does, and for them
|
|
44
|
+
// `relateFolded` carries the ADDITIVE form — `INIT + (ctr << s)`, the init kept whole — as a
|
|
45
|
+
// FALLBACK reached only where the substitutional rule already declined.
|
|
46
|
+
//
|
|
47
|
+
// AND ITS REFUSALS ARE NOT OF A KIND. `relateFolded`'s own three are SPELLINGS of a form that is
|
|
48
|
+
// already sound: the identity holds, this file just does not write `(ctr - start) * (k / d)`, so
|
|
49
|
+
// a round that needs `d = 2` widens it rather than writing a third function beside it. The one
|
|
50
|
+
// SOUNDNESS question is a start that is not a constant at all — it would put a NEW read of the
|
|
51
|
+
// start expression at every use — and `relate` refuses that before `relateFolded` is reached,
|
|
52
|
+
// because none of the five re-evaluation gates asks about it.
|
|
53
|
+
//
|
|
54
|
+
// THE ARITHMETIC. The rewrite rests on one invariant: at every read, `acc == g(ctr)`, where `g` is
|
|
55
|
+
// the init expression with the counter's own start substituted by the counter. It holds at entry
|
|
56
|
+
// because `ctr == start` there, and is preserved because `g` is linear with exactly the
|
|
57
|
+
// accumulator's stride — `g(x + D) - g(x) = K`, checked structurally rather than assumed
|
|
58
|
+
// (`relate`: a `+` spine down to a shift by `s` with `K = D · 2^s`, a product by `M` with
|
|
59
|
+
// `K = D · M`, or the bare counter with `K = D`; any other enclosing operator refuses, because
|
|
60
|
+
// under it the stride is not `K`). Every way the invariant could be broken DECLINES: another
|
|
61
|
+
// write to either name, an address taken, a read outside the loop, a read at or below the
|
|
62
|
+
// accumulator's own step (where it stands one stride ahead of the counter), a `continue` (which
|
|
63
|
+
// runs a `for`'s increment but skips the body's tail), and a step this file cannot relate.
|
|
64
|
+
//
|
|
65
|
+
// THE RE-EVALUATION is the dangerous half, because the closed form is spelled at each read and
|
|
66
|
+
// carries whatever the init READ with it. All five gates below read the ORIGINAL init rather than
|
|
67
|
+
// the substituted form: the init STATEMENT is deleted, so anything inside the counter-start
|
|
68
|
+
// subterm the substitution replaces would be DROPPED rather than moved, which no gate reading the
|
|
69
|
+
// closed form could see.
|
|
70
|
+
//
|
|
71
|
+
// AND THEY ALL ASK ABOUT THE MOTION REGION, which is not the loop. The init is deleted where it
|
|
72
|
+
// stood and re-evaluated at every read, so the distance the values travel is everything between
|
|
73
|
+
// the two — and BOTH ENDS of that span move. The counter's start is the other anchor, because the
|
|
74
|
+
// substitution reads the closed form through it: taken after the init, off a value that has since
|
|
75
|
+
// changed, the closed form is off by exactly that change. So the region opens at whichever anchor
|
|
76
|
+
// comes FIRST and runs to the loop's last iteration, the loop entering whole so the walk reaches
|
|
77
|
+
// its condition and a `for`'s own init and inc as well as its body.
|
|
78
|
+
// • INIT-LOOP-VAR — the init names something the region assigns, so re-evaluating it below that
|
|
79
|
+
// assignment reads a different value. Three shapes, all of which compiled, scored, and
|
|
80
|
+
// computed something else: `for (a0 = a1; …; a0 = a0 + 1)` over `acc = (a1 << 6) + a0` closed
|
|
81
|
+
// to `(a0 << 6) + a0` (a `for`'s counter is stepped in `loop.inc`, which is not in
|
|
82
|
+
// `loop.body`); `acc = (a0 << 6) + a1; a1 = a1 + 100;` above the loop; and the counter's start
|
|
83
|
+
// taken last, `acc = (a0 << 6) + a1; a0 = a0 + 1; i = a0;`. A semantic fuzz over both trees on
|
|
84
|
+
// the same inputs — 18 seeds × 2 generator modes, the second of which emits a statement
|
|
85
|
+
// between the anchors, 1.08M trees and 111707 fired candidates — finds no divergence; asking
|
|
86
|
+
// the LOOP alone put 351 of every 2940 firings on this one hole.
|
|
87
|
+
// • INIT-NAME-ESCAPES — the other half of the same question, for a write no assignment spells.
|
|
88
|
+
// A name whose address the function hands out can be rewritten by a callee or through a
|
|
89
|
+
// stashed pointer, which `assignCount` cannot see: `w = 1; i = a0; acc = (a0 << 6) + w;` over
|
|
90
|
+
// a loop calling `bump(&w)` re-reads `w` once per iteration and picks up whatever the callee
|
|
91
|
+
// left. Refused function-wide, because a stashed pointer outlives the statement that made it.
|
|
92
|
+
// • MOVED-EFFECT — a call or a marker would run once per read instead of once. Refused.
|
|
93
|
+
// • MOVED-VOLATILE — a `volatile` access is one the source pinned precisely so it would not be
|
|
94
|
+
// duplicated or moved. Refused. No corpus row reaches it today (nothing on the default spelling
|
|
95
|
+
// this variation rides carries a qualifier on a READ), so it is guarded by its unit test alone.
|
|
96
|
+
// • MOVED-READ-ALIASABLE — an ordinary memory read moved down the region sees whatever the
|
|
97
|
+
// region wrote. asmlift can only rule that out for writes it can NAME, so a moved read is
|
|
98
|
+
// admitted on one configuration: every write the region evaluates goes to a compile-time
|
|
99
|
+
// constant address INSIDE the target's declared device-register window, and every read lands
|
|
100
|
+
// OUTSIDE it. A device register is not an object a C program declares (target.ts
|
|
101
|
+
// `deviceRegisters`), so no STORE the C performs there can change what an ordinary read sees;
|
|
102
|
+
// the read-side half is what keeps a DEVICE read from being
|
|
103
|
+
// duplicated into N of them, and it resolves an access's WHOLE address where the subscripts
|
|
104
|
+
// are constant, falling back to the chain's root only where they are not (a read rooted at
|
|
105
|
+
// 0x03FFFFF0 whose element is 0x04000010 is a device read, and the root alone does not say
|
|
106
|
+
// so). Anything else — a store through a local pointer, a call, a read rooted at no constant
|
|
107
|
+
// at all — REFUSES, which is `ir/alias.ts`'s posture ("unknown BARS") applied where there is
|
|
108
|
+
// no symbol map to resolve a name through.
|
|
109
|
+
//
|
|
110
|
+
// AND THE PREMISE THAT IS NOT ENOUGH, which this file recorded as a fact about the board and which
|
|
111
|
+
// is FALSE. "A write to a hardware register is not a write to any object a C program declares" is
|
|
112
|
+
// true, and it does not finish the argument: a DMA controller READS a control word and then WRITES
|
|
113
|
+
// ordinary memory on the program's behalf. On the GBA, storing `0x84000020` to `DMA3CNT`
|
|
114
|
+
// (0x040000DC) starts a 32-word transfer into `[DMA3DAD]` — and every row this variation reaches
|
|
115
|
+
// drives exactly that register. Modelled and executed, the admitted candidate turns a clean walk
|
|
116
|
+
// over a destination table into wild writes: the first transfer clobbers the table the init reads,
|
|
117
|
+
// and every later iteration recomputes its destination from the garbage.
|
|
118
|
+
//
|
|
119
|
+
// So the function's device writes are checked against `capabilities.deviceMemoryWriters` — the
|
|
120
|
+
// four DMA channel-enable halfwords on this board — and a moved read under one is NOT admitted on
|
|
121
|
+
// the gates alone. That scan is the WHOLE PREFIX up to and including the loop, wider than the
|
|
122
|
+
// motion region every other gate reads, because a repeating transfer keeps writing for as long as
|
|
123
|
+
// it is enabled: where the arming store STANDS says nothing about when the device writes, and a
|
|
124
|
+
// channel armed above the init is as asynchronous as one armed inside the loop.
|
|
125
|
+
//
|
|
126
|
+
// Such a read is admitted only where the differ PROVES it: `needsProof` rides out with the
|
|
127
|
+
// candidate, and rank.ts publishes such a spelling only at a byte-exact score,
|
|
128
|
+
// withholding it (loudly, in `RankedResult.withheld`) at every other. That is not a softening of
|
|
129
|
+
// the rule but the only evidence that settles it — a candidate whose object equals the target's
|
|
130
|
+
// IS the program, whatever a gate could have proved about it, and the one corpus inhabitant
|
|
131
|
+
// (synthetic:dmaptrsrc) is exactly that: a byte-exact match whose reference source really does
|
|
132
|
+
// read `gBg[bg].pTilemap` inside the loop. Barring it instead costs that match and buys nothing —
|
|
133
|
+
// the sound alternative, the read hoisted into a local above the loop, scores 16, because a C
|
|
134
|
+
// statement lands above the loop's ENTRY GUARD while the compiler's own hoist lands below it.
|
|
135
|
+
//
|
|
136
|
+
// AND THE STRIDE'S UNITS, which is the half of the arithmetic the invariant above hides. `k` is
|
|
137
|
+
// read off `acc = acc + K`, so it counts in the units of the ACCUMULATOR's declared type: on a
|
|
138
|
+
// `u16 *` a step of 32 advances 64 BYTES. The closed form spells that stride onto the INIT, whose
|
|
139
|
+
// `+` scales by whatever the INIT's own C type says. Where the two disagree the candidate
|
|
140
|
+
// addresses the wrong byte, compiles clean, and carries no marker — structure.ts's `bytePtr`
|
|
141
|
+
// states the same rule from the other end ("a `u16 *` walked by a computed offset addresses TWICE
|
|
142
|
+
// the intended byte, and nothing downstream can see the error"). `stride-units` refuses unless
|
|
143
|
+
// both scales are KNOWN and equal; a narrow integer accumulator is the same question in the other
|
|
144
|
+
// direction, since `u16 acc` wraps at 65536 where `init + (i << 6)` does not.
|
|
145
|
+
//
|
|
146
|
+
// THAT GATE HAS NO BENCHMARK REACH AT ALL, and neither tier can see it. Censused at the
|
|
147
|
+
// `firstRejection` call site over both — 750 synthetic trees and all 252 real-tier rows, the real
|
|
148
|
+
// tier being the SYMBOL-MAPPED configuration since every row carries its authored map —
|
|
149
|
+
// `unitsDisagree` is true on NO row in either, and `/unreduce` fires on no real-tier row at all,
|
|
150
|
+
// so no zero-flip gate reaches this variation. Its one known inhabitant is outside the benchmark:
|
|
151
|
+
// klonoa's `UpdateHUDTimePanel`, where WITH a symbol map the accumulator lifts `u16 *` against an
|
|
152
|
+
// integer init and the gate refuses it, and with RAW ADDRESSES the same asm lifts all-integer and
|
|
153
|
+
// the candidate is correct and survives (`50335396 + (v15 << 6)`, 64 bytes an iteration, which is
|
|
154
|
+
// the ROM's own `adds r1, #0x40`). Same assembly, same loop, same stride — a raw-address sweep is
|
|
155
|
+
// BLIND to the defect and reports the variation as correct. The checkout sweep behind that datum
|
|
156
|
+
// covered klonoa's 467 functions in BOTH configurations; a second checkout was swept raw-only and
|
|
157
|
+
// found nothing, and since raw-only is the blind arm that null result carries no weight. Only the
|
|
158
|
+
// klonoa half of the sweep is evidence, and it is quoted here without the other.
|
|
159
|
+
//
|
|
160
|
+
// SCOPE, stated because a decline outside it names no gate and so looks exactly like a gate that
|
|
161
|
+
// refused. This pass walks TOP-LEVEL loops only: the counter's start and the accumulator's init are
|
|
162
|
+
// found by scanning `sfn.body` above the loop, which is a flat list. A loop under an `if` — or
|
|
163
|
+
// inside another loop — is never reached, even when both statements do stand above it in the
|
|
164
|
+
// enclosing block. Measured over the corpus in both symbol-map configurations: of 834 trees, 189
|
|
165
|
+
// carry a loop, 98 carry a TOP-LEVEL one, and 91 carry only nested ones — `arraysum`, `memcpy1`,
|
|
166
|
+
// `revarr`, `dotprod`, `findfirst`, `mergeloop` and `synthetic:dmanest` among them. On klonoa's
|
|
167
|
+
// `LoadBGTilemapData` the count is zero, over all 1344 trees its enumeration produces: a decline
|
|
168
|
+
// there names no gate, and a reader will attribute one anyway. Widening the scan is a REACH change
|
|
169
|
+
// and belongs to a row that demands it, not to a soundness pass.
|
|
170
|
+
//
|
|
171
|
+
// AND THE TABLE ANSWERS FOR A SMALLER POPULATION STILL. Censused at the `firstRejection` call
|
|
172
|
+
// site over the benchmark's two tiers, counting FIRST rejections rather than reach — short-circuit
|
|
173
|
+
// order hides a later gate behind an earlier one. Of 750 synthetic trees, 21 (loop, accumulator)
|
|
174
|
+
// pairs reach the table: 8 admit, `acc-live-outside` 7, `acc-read-at-step` 4, `unrelated-start` 2.
|
|
175
|
+
// Of 252 real-tier rows, 6 reach it: `acc-live-outside` 3, and one each of `acc-multi-assign`,
|
|
176
|
+
// `acc-read-at-step` and `unrelated-start`. FOUR of the twenty gates decide anything; the rest —
|
|
177
|
+
// `moved-read-aliasable`, which the device-memory argument above rests on, among them — are held
|
|
178
|
+
// by their unit tests and by the fuzz, and by nothing either tier has yet shown them.
|
|
179
|
+
//
|
|
180
|
+
// AND ITS SIBLING. `l3/reindex.ts` un-reduces a POINTER WALK over the same argument, with the same
|
|
181
|
+
// shape of gate table, and it already handles the `if (guard) do {} while` rotation this file
|
|
182
|
+
// cannot see. The split is by the induction variable's TYPE rather than by the question asked, and
|
|
183
|
+
// it costs the duplication a reader will notice — `counter-roles` ≈ `acc-multi-assign` +
|
|
184
|
+
// `acc-live-outside`, `walk-stride` ≈ `scale-mismatch`, `body-exit` ≈ `continue-in-body`. Folding
|
|
185
|
+
// them into one pass over one table is a real improvement and a real refactor; the gate this file
|
|
186
|
+
// was actually MISSING from that table (`volatile-counter`) is in it now, which is the part that
|
|
187
|
+
// could not wait.
|
|
188
|
+
//
|
|
189
|
+
// Nothing qualifying ⇒ decline (null), never a duplicate of the default.
|
|
190
|
+
import { type IrType } from '../ir/types';
|
|
191
|
+
import { cellAddress, inRange, rootConst } from './address';
|
|
192
|
+
import {
|
|
193
|
+
type Expr,
|
|
194
|
+
type SFn,
|
|
195
|
+
type Stmt,
|
|
196
|
+
exprEquals,
|
|
197
|
+
exprHasEffect,
|
|
198
|
+
exprReadsVolatile,
|
|
199
|
+
mapExprChildren,
|
|
200
|
+
stmtChildren,
|
|
201
|
+
stmtExprs,
|
|
202
|
+
walkExprs,
|
|
203
|
+
} from './ast';
|
|
204
|
+
import { type Gate, firstRejection } from './gates';
|
|
205
|
+
import { type VarTypes, declaredTypes, exprCType, ptrElemBytes } from './typing';
|
|
206
|
+
|
|
207
|
+
/** Why `relate` refused — one tag per question it asks. `relate` is the only place that knows
|
|
208
|
+
* which question failed, so it says so; the five gates below test one tag each. */
|
|
209
|
+
type RelDecline = 'scale-mismatch' | 'unrelated-start' | 'nonzero-start' | 'step-ratio' | 'stride-not-shift';
|
|
210
|
+
|
|
211
|
+
/** `relate`'s answer: the closed form, or the reason there is none. */
|
|
212
|
+
type Relation = { readonly ok: Expr } | { readonly declined: RelDecline };
|
|
213
|
+
|
|
214
|
+
/** One (loop, accumulator) pair as the gates read it. */
|
|
215
|
+
export interface AccCtx {
|
|
216
|
+
/** the accumulator is assigned exactly twice: its init above the loop and its step inside */
|
|
217
|
+
assigns: number;
|
|
218
|
+
addrTaken: boolean;
|
|
219
|
+
/** the local's DECLARATION carries an asm fact — a qualifier, a frame slot, an `undef` */
|
|
220
|
+
pinned: boolean;
|
|
221
|
+
/** the accumulator is mentioned outside the loop, other than by its own init */
|
|
222
|
+
liveOutside: boolean;
|
|
223
|
+
/** the accumulator is mentioned at or below its own step, or in the loop's control parts */
|
|
224
|
+
readAtOrBelowStep: boolean;
|
|
225
|
+
/** the counter is assigned exactly twice: its init above the loop and its step inside */
|
|
226
|
+
counterAssigns: number;
|
|
227
|
+
counterAddrTaken: boolean;
|
|
228
|
+
/** the counter local carries a volatility qualifier — every closed form is a new read of it */
|
|
229
|
+
counterVolatile: boolean;
|
|
230
|
+
/** a `continue` anywhere in the body */
|
|
231
|
+
hasContinue: boolean;
|
|
232
|
+
/** the accumulator's own step and its init count in DIFFERENT C units, so the closed form's
|
|
233
|
+
* `+` would scale by the wrong element size (or by none) */
|
|
234
|
+
unitsDisagree: boolean;
|
|
235
|
+
/** WHY `relate` refused, or null where it produced a closed form — one tag per question it
|
|
236
|
+
* asks, so each of the five relation gates tests exactly one reason. */
|
|
237
|
+
declined: RelDecline | null;
|
|
238
|
+
/** the init reads a name something in the motion region assigns */
|
|
239
|
+
initLoopVar: boolean;
|
|
240
|
+
/** the init reads a name whose address the function hands out */
|
|
241
|
+
initNameEscapes: boolean;
|
|
242
|
+
/** the closed form contains a call or a marker */
|
|
243
|
+
movedEffect: boolean;
|
|
244
|
+
/** the closed form reads a `volatile` object */
|
|
245
|
+
movedVolatile: boolean;
|
|
246
|
+
/** the closed form reads memory the region's own writes cannot be told apart from */
|
|
247
|
+
movedAliasable: boolean;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
|
|
251
|
+
{
|
|
252
|
+
id: 'acc-multi-assign',
|
|
253
|
+
why: 'a name written anywhere but its init and its step is not one induction sequence',
|
|
254
|
+
sound: true,
|
|
255
|
+
guardedBy: 'unreduce.test.ts: a third assignment to the accumulator declines',
|
|
256
|
+
rejects: (c) => c.assigns !== 2,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: 'acc-addr-taken',
|
|
260
|
+
why: 'a deleted local has no address to take',
|
|
261
|
+
sound: true,
|
|
262
|
+
guardedBy: 'unreduce.test.ts: an address-taken accumulator declines',
|
|
263
|
+
rejects: (c) => c.addrTaken,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'acc-pinned',
|
|
267
|
+
why: 'a declaration that carries a fact from the assembly cannot be deleted without dropping the fact',
|
|
268
|
+
sound: true,
|
|
269
|
+
guardedBy: 'unreduce.test.ts: a pinned accumulator declines, on every pin a local can carry',
|
|
270
|
+
rejects: (c) => c.pinned,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'stride-units',
|
|
274
|
+
why: 'a step counted in the accumulator’s own units is not the init’s, and the closed form would scale by the wrong one',
|
|
275
|
+
sound: true,
|
|
276
|
+
guardedBy: 'unreduce.test.ts: an accumulator whose step counts different units than its init declines',
|
|
277
|
+
rejects: (c) => c.unitsDisagree,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'acc-live-outside',
|
|
281
|
+
why: 'a read outside the loop wants a value the closed form no longer computes',
|
|
282
|
+
sound: true,
|
|
283
|
+
guardedBy: 'unreduce.test.ts: an accumulator read after the loop declines',
|
|
284
|
+
rejects: (c) => c.liveOutside,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'acc-read-at-step',
|
|
288
|
+
why: 'below its own step the accumulator is one stride ahead of the counter',
|
|
289
|
+
sound: true,
|
|
290
|
+
guardedBy: 'unreduce.test.ts: a read below the step declines',
|
|
291
|
+
rejects: (c) => c.readAtOrBelowStep,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'counter-multi-assign',
|
|
295
|
+
why: 'a counter written elsewhere breaks the relation the closed form is read through',
|
|
296
|
+
sound: true,
|
|
297
|
+
guardedBy: 'unreduce.test.ts: a counter assigned inside an arm declines',
|
|
298
|
+
rejects: (c) => c.counterAssigns !== 2,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: 'counter-addr-taken',
|
|
302
|
+
why: 'an address-taken counter can be stepped by anything the address reaches',
|
|
303
|
+
sound: true,
|
|
304
|
+
guardedBy: 'unreduce.test.ts: an address-taken counter declines',
|
|
305
|
+
rejects: (c) => c.counterAddrTaken,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
// The accumulator's pin is `acc-pinned` above; this is the COUNTER's, and it is a different
|
|
309
|
+
// fact: substitution puts the counter where every accumulator read used to be, so a loop that
|
|
310
|
+
// read it once per iteration reads it once per USE. For a volatile object the access COUNT is
|
|
311
|
+
// the semantics (l3/volatileval.ts states the same rule), which is why l3/reindex.ts's
|
|
312
|
+
// `volatile-counter` sibling exists.
|
|
313
|
+
id: 'counter-volatile',
|
|
314
|
+
why: 'the closed form re-reads the counter at every use, and a volatile object counts its reads',
|
|
315
|
+
sound: true,
|
|
316
|
+
guardedBy: 'unreduce.test.ts: a volatile counter declines',
|
|
317
|
+
rejects: (c) => c.counterVolatile,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'continue-in-body',
|
|
321
|
+
why: 'a `continue` runs a `for`’s increment but skips the body’s tail, desynchronizing the pair',
|
|
322
|
+
sound: true,
|
|
323
|
+
guardedBy: 'unreduce.test.ts: a `continue` in the body declines',
|
|
324
|
+
rejects: (c) => c.hasContinue,
|
|
325
|
+
},
|
|
326
|
+
// ── the five refusals `relate` can produce, one gate each ─────────────────────────────────
|
|
327
|
+
//
|
|
328
|
+
// These five PARTITION the non-null `declined` tags, and each is keyed on the decision `relate`
|
|
329
|
+
// took rather than on a tree fact that stands for it (`the start is a constant`): a proxy makes
|
|
330
|
+
// the `why` drift the moment a second reason shares the fact.
|
|
331
|
+
{
|
|
332
|
+
id: 'scale-mismatch',
|
|
333
|
+
why: 'an init whose scale does not carry the accumulator’s whole stride proves nothing',
|
|
334
|
+
sound: true,
|
|
335
|
+
guardedBy: 'unreduce.test.ts: a stride that does not match the init’s scale declines',
|
|
336
|
+
rejects: (c) => c.declined === 'scale-mismatch',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: 'unrelated-start',
|
|
340
|
+
why: 'a symbolic start the init does not name exactly once leaves nothing to substitute for',
|
|
341
|
+
sound: true,
|
|
342
|
+
guardedBy: 'unreduce.test.ts: an init that never names the counter declines',
|
|
343
|
+
rejects: (c) => c.declined === 'unrelated-start',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: 'nonzero-start',
|
|
347
|
+
why: 'a counter starting at a nonzero constant leaves a bias term this rewrite does not write',
|
|
348
|
+
sound: true,
|
|
349
|
+
guardedBy: 'unreduce.test.ts: a counter-free init declines unless its start is the constant 0',
|
|
350
|
+
rejects: (c) => c.declined === 'nonzero-start',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'step-ratio',
|
|
354
|
+
why: 'a counter stepping by more than one leaves a ratio between the two steps, which is not a shift',
|
|
355
|
+
sound: true,
|
|
356
|
+
guardedBy: 'unreduce.test.ts: a counter-free init declines when the counter does not step by one',
|
|
357
|
+
rejects: (c) => c.declined === 'step-ratio',
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: 'stride-not-shift',
|
|
361
|
+
why: 'a stride that is not a constant power of two has no shift to carry it',
|
|
362
|
+
sound: true,
|
|
363
|
+
guardedBy: 'unreduce.test.ts: a counter-free init declines when the accumulator’s stride is not a power of two',
|
|
364
|
+
rejects: (c) => c.declined === 'stride-not-shift',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: 'init-loop-var',
|
|
368
|
+
why: 'a name the region assigns reads differently once the init is evaluated below it',
|
|
369
|
+
sound: true,
|
|
370
|
+
guardedBy: 'unreduce.test.ts: an init reading a name the region assigns declines, in every part of it',
|
|
371
|
+
rejects: (c) => c.initLoopVar,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
// `init-loop-var`'s other half. That gate reads C-LEVEL assignment, and a local whose address
|
|
375
|
+
// the function hands out is written where no assignment spells it — by a callee, or through a
|
|
376
|
+
// pointer the region stores into. The address is taken function-wide because a stashed pointer
|
|
377
|
+
// outlives the statement that made it.
|
|
378
|
+
id: 'init-name-escapes',
|
|
379
|
+
why: 'an address-escaped name is written where no assignment names it',
|
|
380
|
+
sound: true,
|
|
381
|
+
guardedBy: 'unreduce.test.ts: an init reading an address-escaped local declines',
|
|
382
|
+
rejects: (c) => c.initNameEscapes,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'moved-effect',
|
|
386
|
+
why: 'a call or a marker in the closed form would run once per read instead of once',
|
|
387
|
+
sound: true,
|
|
388
|
+
guardedBy: 'unreduce.test.ts: a call in the init declines',
|
|
389
|
+
rejects: (c) => c.movedEffect,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 'moved-volatile',
|
|
393
|
+
why: 'a volatile access is one the source pinned so it would not be duplicated or moved',
|
|
394
|
+
sound: true,
|
|
395
|
+
guardedBy: 'unreduce.test.ts: a volatile read in the init declines',
|
|
396
|
+
rejects: (c) => c.movedVolatile,
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: 'moved-read-aliasable',
|
|
400
|
+
why: 'a read moved down the region sees whatever writes the region cannot be proved apart from',
|
|
401
|
+
sound: true,
|
|
402
|
+
guardedBy: 'unreduce.test.ts: a moved read declines unless the region writes only device cells',
|
|
403
|
+
rejects: (c) => c.movedAliasable,
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
// ── the induction shapes ────────────────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
/** The BYTE scale of one unit of C arithmetic on a value of this type — what `x + 1` advances
|
|
410
|
+
* `x` by. A pointer scales by its pointee; a full-width integer scales by one. Everything else is
|
|
411
|
+
* `null` = NOT ADMISSIBLE rather than a guess: a struct/void/array pointee has no scale this file
|
|
412
|
+
* can name (`ptrElemBytes` returns 0 for exactly those), a narrow integer WRAPS where the closed
|
|
413
|
+
* form does not, and an unknown type is unknown. `stride-units` compares the accumulator's
|
|
414
|
+
* declared scale against the init expression's, and refuses unless both are known and equal. */
|
|
415
|
+
function arithScale(t: IrType | undefined): number | null {
|
|
416
|
+
if (t === undefined) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
if (t.kind === 'ptr') {
|
|
420
|
+
const bytes = ptrElemBytes(t.to);
|
|
421
|
+
return bytes > 0 ? bytes : null;
|
|
422
|
+
}
|
|
423
|
+
return t.kind === 'int' && t.width === 32 ? 1 : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Does this local's DECLARATION pin it against deletion? Every flag `SFn.locals` can carry, because
|
|
427
|
+
* each is a fact about the ASM that only the declaration states: two qualifiers (deleting a
|
|
428
|
+
* `volatile u16 *` local re-spells `*p = 0` as a raw cast with no qualifier on it — l3/inlinebase.ts
|
|
429
|
+
* carries it onto the minted cast instead, and this variation has no local left to carry anything), a
|
|
430
|
+
* frame home, an `undef` whose whole content is the assignment that is MISSING, and the SPILL HOMES.
|
|
431
|
+
*
|
|
432
|
+
* WHY `slots` PINS, which is not the obvious reading. Deleting a slot-carrying local does not
|
|
433
|
+
* mis-order the survivors: they stay a subset of one total order and rank correctly among
|
|
434
|
+
* themselves. What it can do is flip a REFUSAL into an ordering. `l3/slotorder.ts` refuses the whole
|
|
435
|
+
* function when two declared locals share one offset, because reload hands each spilled pseudo a
|
|
436
|
+
* fresh slot and a duplicate proves the offsets did not come from reload. Delete one sharer and the
|
|
437
|
+
* survivors are injective — so the ordering fires on a frame whose evidence was already known not to
|
|
438
|
+
* be declaration ranks, and it fires silently. Refusing to delete keeps the duplicate, and keeps the
|
|
439
|
+
* refusal.
|
|
440
|
+
*
|
|
441
|
+
* MEASURED, so this is a stated zero and not an assumption: instrumented at the deletion site, it
|
|
442
|
+
* fires on none of the three agbcc rows that spill AND lift — `spillorder`, `dma_fill_uninit`,
|
|
443
|
+
* `uninit_spill` — so the clause costs no candidate today. (`spill10` spills too but declines in the
|
|
444
|
+
* Thumb frontend, so it never reaches this pass and its zero says nothing.) It is here for the day
|
|
445
|
+
* one does. */
|
|
446
|
+
function declarationPins(l: SFn['locals'][number]): boolean {
|
|
447
|
+
return (
|
|
448
|
+
l.volatile !== undefined ||
|
|
449
|
+
l.pointeeVolatile !== undefined ||
|
|
450
|
+
l.frame !== undefined ||
|
|
451
|
+
l.uninit !== undefined ||
|
|
452
|
+
l.slots !== undefined
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Read a constant the FRONTEND spelled as arithmetic as its VALUE. Thumb's `add rd, #imm8`
|
|
457
|
+
* cannot spell 256, so agbcc emits `mov #128 / lsl #1` and the recovered node is `128 << 1`.
|
|
458
|
+
* Every question the relation asks about a constant is about its value — the accumulator's
|
|
459
|
+
* stride, the counter's start, the counter's step, a product's invariant multiplier — so each is
|
|
460
|
+
* folded before it is read; unfolded, the same number refuses on its spelling and names a gate
|
|
461
|
+
* whose `why` is about the value (synthetic:offgiv3 is the row, on the stride).
|
|
462
|
+
*
|
|
463
|
+
* ONLY all-constant nodes fold, which is what keeps this from being a general simplifier: nothing
|
|
464
|
+
* that mentions a name is touched, so `acc-read-at-step` — which reads the same expression for
|
|
465
|
+
* the accumulator's own name — sees exactly the names it saw before. */
|
|
466
|
+
function foldConsts(e: Expr): Expr {
|
|
467
|
+
if (e.k !== 'bin') {
|
|
468
|
+
return e;
|
|
469
|
+
}
|
|
470
|
+
const l = foldConsts(e.l);
|
|
471
|
+
const r = foldConsts(e.r);
|
|
472
|
+
if (l.k !== 'const' || r.k !== 'const') {
|
|
473
|
+
return e;
|
|
474
|
+
}
|
|
475
|
+
// int32 arithmetic, because that is what the machine did and what the C will do
|
|
476
|
+
switch (e.op) {
|
|
477
|
+
case '+':
|
|
478
|
+
return { k: 'const', value: (l.value + r.value) | 0 };
|
|
479
|
+
case '*':
|
|
480
|
+
return { k: 'const', value: Math.imul(l.value, r.value) };
|
|
481
|
+
case '<<':
|
|
482
|
+
return r.value >= 0 && r.value < 32 ? { k: 'const', value: l.value << r.value } : e;
|
|
483
|
+
default:
|
|
484
|
+
return e;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** `name = name + <expr>` as a step, or null. */
|
|
489
|
+
function stepOf(s: Stmt, name: string): Expr | null {
|
|
490
|
+
if (s.k !== 'assign' || s.name !== name || s.value.k !== 'bin' || s.value.op !== '+') {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
const { l, r } = s.value;
|
|
494
|
+
return l.k === 'var' && l.name === name ? r : r.k === 'var' && r.name === name ? l : null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** THE relation check: is `init` a function of `start` whose value grows by `k` per `d` of the
|
|
498
|
+
* counter? Returns the closed form (init with the counter's start replaced by the counter
|
|
499
|
+
* variable) or null. The three accepted shapes are the three ways a compiler's own giv is
|
|
500
|
+
* spelled — a scaled shift, a product, and the bare index — and each is verified rather than
|
|
501
|
+
* assumed: the substituted subterm must be structurally the counter's start, and the stride must
|
|
502
|
+
* come out of the scale. */
|
|
503
|
+
function relate(init: Expr, start: Expr, ctr: string, k: Expr, d: number): Relation {
|
|
504
|
+
const kConst = k.k === 'const' ? k.value : null;
|
|
505
|
+
const idx = (): Expr => ({ k: 'var', name: ctr });
|
|
506
|
+
const holds = (e: Expr): boolean => [...subterms(e)].some((x) => exprEquals(x, start));
|
|
507
|
+
// The path from the init's root down to the occurrence, one node at a time. Every node on it
|
|
508
|
+
// must be a `+` — so the init is a SUM of the scaled counter and terms that do not mention it,
|
|
509
|
+
// and the whole expression's stride is the scaled term's. Any other enclosing operator refuses:
|
|
510
|
+
// under a `-` on the right the stride flips sign, and under a second scale it multiplies.
|
|
511
|
+
const rec = (x: Expr): Expr | null => {
|
|
512
|
+
// (a) `start << s` — the stride is `d << s`, so `k` has to be that constant
|
|
513
|
+
if (x.k === 'bin' && x.op === '<<' && exprEquals(x.l, start) && x.r.k === 'const') {
|
|
514
|
+
const sh = x.r.value;
|
|
515
|
+
return sh >= 0 && sh < 31 && kConst === d * 2 ** sh ? { k: 'bin', op: '<<', l: idx(), r: x.r } : null;
|
|
516
|
+
}
|
|
517
|
+
// (b) `start * M` in either order — the stride is `d · M`, checkable when `d` is 1 and `M` is
|
|
518
|
+
// structurally `k`, or when both are constants. The OPERAND ORDER is kept: which side a
|
|
519
|
+
// product's index sits on is a spelling the differ referees on its own (`/mulfirst`), so
|
|
520
|
+
// rebuilding it in a canonical order would answer that question here instead.
|
|
521
|
+
if (x.k === 'bin' && x.op === '*' && (exprEquals(x.l, start) || exprEquals(x.r, start))) {
|
|
522
|
+
const startLeft = exprEquals(x.l, start);
|
|
523
|
+
const m = foldConsts(startLeft ? x.r : x.l);
|
|
524
|
+
const ok = d === 1 ? exprEquals(m, k) : m.k === 'const' && kConst !== null && d * m.value === kConst;
|
|
525
|
+
return ok ? { k: 'bin', op: '*', l: startLeft ? idx() : m, r: startLeft ? m : idx() } : null;
|
|
526
|
+
}
|
|
527
|
+
// (c) the counter standing on its own in the sum — the stride is `d` itself
|
|
528
|
+
if (exprEquals(x, start)) {
|
|
529
|
+
return kConst === d ? idx() : null;
|
|
530
|
+
}
|
|
531
|
+
// (d) a `+` node: descend into whichever side carries the occurrence. There is exactly one.
|
|
532
|
+
if (x.k === 'bin' && x.op === '+') {
|
|
533
|
+
const left = holds(x.l);
|
|
534
|
+
const inner = rec(left ? x.l : x.r);
|
|
535
|
+
return inner === null ? null : left ? { ...x, l: inner } : { ...x, r: inner };
|
|
536
|
+
}
|
|
537
|
+
return null; // anything else between the root and the counter, and the stride is not `k`
|
|
538
|
+
};
|
|
539
|
+
// THE SUBSTITUTIONAL FORM FIRST, unchanged: it is the shipped spelling every corpus inhabitant
|
|
540
|
+
// of this variation rides, and trying it first makes the branch below strictly additive — it is
|
|
541
|
+
// reached only where the old rule already declined, so it can admit candidates but never
|
|
542
|
+
// re-spell one.
|
|
543
|
+
//
|
|
544
|
+
// `!== 1` — zero: the init does not depend on the counter. two: which one is the index?
|
|
545
|
+
const occurrences = startOccurrences(init, start);
|
|
546
|
+
const substituted = occurrences === 1 ? rec(init) : null;
|
|
547
|
+
if (substituted !== null) {
|
|
548
|
+
return { ok: substituted };
|
|
549
|
+
}
|
|
550
|
+
// …AND THE FOLDED FORM AS THE FALLBACK. The occurrence COUNT cannot route this on its own:
|
|
551
|
+
// where the start is the constant 0, every unrelated literal zero in the init — an `[0]`
|
|
552
|
+
// subscript, a `+ 0` — `exprEquals` the start and counts as one. `rec` never substitutes for
|
|
553
|
+
// such a literal (it verifies an all-`+` spine down to the occurrence at the accumulator's own
|
|
554
|
+
// scale, and an `index` node on that path refuses), so running it FIRST and asking about the
|
|
555
|
+
// start's SHAPE second misroutes nothing.
|
|
556
|
+
// The start's SHAPE is what routes this, so it is read as a VALUE: `128 << 1` is the constant
|
|
557
|
+
// 256, and taking it for a symbolic start would name a gate whose `why` says the start is one.
|
|
558
|
+
// `rec` above keeps matching the init's subterms against the ORIGINAL spelling, which is what
|
|
559
|
+
// the init carries.
|
|
560
|
+
const startConst = foldConsts(start);
|
|
561
|
+
if (startConst.k !== 'const') {
|
|
562
|
+
// A symbolic start cannot have folded, so the init either does not name it at all or names it
|
|
563
|
+
// twice (and which occurrence is the index is not decidable) — `unrelated-start`; or it names
|
|
564
|
+
// it once and `rec` refused the scale — `scale-mismatch`.
|
|
565
|
+
return { declined: occurrences === 1 ? 'scale-mismatch' : 'unrelated-start' };
|
|
566
|
+
}
|
|
567
|
+
return relateFolded(init, startConst.value, ctr, k, d);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** THE OTHER SPELLING of the identity above: ADDITIVE, `INIT + (ctr << s)`, with the init kept
|
|
571
|
+
* whole and the scaled counter added to it. It applies where the counter's start has been folded
|
|
572
|
+
* out of the init and there is nothing left to substitute for (see the header). `start` is the
|
|
573
|
+
* start's VALUE — `relate` has already established that it is a constant.
|
|
574
|
+
*
|
|
575
|
+
* The invariant is the header's, and holds for the same reason: at entry `ctr == 0`, so
|
|
576
|
+
* `INIT + (0 << s) == INIT`; per iteration `ctr` grows by 1 and the closed form by `2^s`, which
|
|
577
|
+
* is the accumulator's own stride `k`. All five re-evaluation gates read `initStmt.value` — the
|
|
578
|
+
* ORIGINAL init — and that is exactly what this form re-evaluates at each read, so none of them
|
|
579
|
+
* needs a second reading here.
|
|
580
|
+
*
|
|
581
|
+
* SCOPE. Three refusals, each with its own tag and so its own gate and its own `why`:
|
|
582
|
+
* • `nonzero-start` — the start is a constant other than 0. Sound and merely unspelled: it
|
|
583
|
+
* wants the bias term `- start * k`, which no corpus row asks for. A start that is not a
|
|
584
|
+
* constant AT ALL never arrives here; `relate` answers that one, with `unrelated-start` or
|
|
585
|
+
* `scale-mismatch`.
|
|
586
|
+
* • `step-ratio` — `d` is not 1, so the closed form would carry the ratio `K / d`.
|
|
587
|
+
* • `stride-not-shift` — `k` is not a power of two once folded. `INIT + ctr * k` is the general
|
|
588
|
+
* spelling and compiles identically at agbcc (both were compiled and diffed: byte-identical),
|
|
589
|
+
* so a second spelling would double the fan and buy no score; the shift is what this class's
|
|
590
|
+
* references spell.
|
|
591
|
+
* All three first-reject on NO row in either tier — the census above lists the four gates that
|
|
592
|
+
* decide anything, and none of these is one of them — so all three are held by their unit tests
|
|
593
|
+
* alone, on the shipped precedent of `moved-volatile`. `offgiv3` is not backing for them: it
|
|
594
|
+
* MATCHes, so it rejects at no gate at all. */
|
|
595
|
+
function relateFolded(init: Expr, start: number, ctr: string, k: Expr, d: number): Relation {
|
|
596
|
+
if (start !== 0) {
|
|
597
|
+
return { declined: 'nonzero-start' };
|
|
598
|
+
}
|
|
599
|
+
if (d !== 1) {
|
|
600
|
+
return { declined: 'step-ratio' };
|
|
601
|
+
}
|
|
602
|
+
const step = k.k === 'const' ? k.value : 0;
|
|
603
|
+
// not a constant power of two in shift range, so no shift carries the stride
|
|
604
|
+
if (step <= 0 || (step & (step - 1)) !== 0 || Math.log2(step) >= 31) {
|
|
605
|
+
return { declined: 'stride-not-shift' };
|
|
606
|
+
}
|
|
607
|
+
const sh = Math.log2(step);
|
|
608
|
+
const idx: Expr = { k: 'var', name: ctr };
|
|
609
|
+
// `sh === 0` is the counter standing on its own — `i << 0` is the same value spelled worse, and
|
|
610
|
+
// `rec`'s branch (c) already writes the bare counter for the substitutional case.
|
|
611
|
+
const scaled: Expr = sh === 0 ? idx : { k: 'bin', op: '<<', l: idx, r: { k: 'const', value: sh } };
|
|
612
|
+
// A ZERO init contributes nothing and `0 + (i << 3)` is a spelling no source writes, so the
|
|
613
|
+
// scaled counter stands alone — this is `rec`'s branch (c) reached the other way.
|
|
614
|
+
// (synthetic:nestedloop:mwcc_242_81 is such an accumulator.)
|
|
615
|
+
//
|
|
616
|
+
// Otherwise the INIT stays on the LEFT: it is the base the source names, and `rec` builds the
|
|
617
|
+
// same shape for the same loop where the fold did not happen. Product operand order is
|
|
618
|
+
// `/mulfirst`'s question, not this file's.
|
|
619
|
+
return { ok: init.k === 'const' && init.value === 0 ? scaled : { k: 'bin', op: '+', l: init, r: scaled } };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** how many times the counter's start stands as a subterm of the init. ONE is the substitutional
|
|
623
|
+
* case; two is ambiguous (which occurrence is the index?); zero is the folded case. */
|
|
624
|
+
const startOccurrences = (init: Expr, start: Expr): number =>
|
|
625
|
+
[...subterms(init)].filter((x) => exprEquals(x, start)).length;
|
|
626
|
+
|
|
627
|
+
/** every node of an expression tree, itself included */
|
|
628
|
+
function* subterms(e: Expr): Generator<Expr> {
|
|
629
|
+
yield e;
|
|
630
|
+
for (const c of exprChildrenOf(e)) {
|
|
631
|
+
yield* subterms(c);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const exprChildrenOf = (e: Expr): Expr[] => {
|
|
636
|
+
const out: Expr[] = [];
|
|
637
|
+
mapExprChildren(e, (c) => {
|
|
638
|
+
out.push(c);
|
|
639
|
+
return c;
|
|
640
|
+
});
|
|
641
|
+
return out;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// ── what a tree does to a name ──────────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
/** assignments to `name` anywhere in these statements, `for` init/inc included */
|
|
647
|
+
function assignCount(stmts: readonly Stmt[], name: string): number {
|
|
648
|
+
let n = 0;
|
|
649
|
+
for (const s of stmts) {
|
|
650
|
+
if (s.k === 'assign' && s.name === name) {
|
|
651
|
+
n++;
|
|
652
|
+
}
|
|
653
|
+
n += assignCount(stmtChildren(s), name);
|
|
654
|
+
}
|
|
655
|
+
return n;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** does any expression in these statements read `name`? */
|
|
659
|
+
function mentions(stmts: readonly Stmt[], name: string): boolean {
|
|
660
|
+
for (const e of walkExprs(stmts as Stmt[])) {
|
|
661
|
+
if (e.k === 'var' && e.name === name) {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const mentionsIn = (e: Expr, name: string): boolean => [...subterms(e)].some((x) => x.k === 'var' && x.name === name);
|
|
669
|
+
|
|
670
|
+
const addrTakenIn = (stmts: readonly Stmt[], name: string): boolean => {
|
|
671
|
+
for (const e of walkExprs(stmts as Stmt[])) {
|
|
672
|
+
if (e.k === 'addr' && e.name === name) {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return false;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const hasContinueIn = (stmts: readonly Stmt[]): boolean =>
|
|
680
|
+
stmts.some((s) => s.k === 'continue' || hasContinueIn(stmtChildren(s)));
|
|
681
|
+
|
|
682
|
+
// ── the re-evaluation gates ─────────────────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
/** every memory access in an expression, as its own node */
|
|
685
|
+
const accessesIn = (e: Expr): (Extract<Expr, { k: 'index' }> | Extract<Expr, { k: 'field' }>)[] =>
|
|
686
|
+
[...subterms(e)].filter((x): x is Extract<Expr, { k: 'index' | 'field' }> => x.k === 'index' || x.k === 'field');
|
|
687
|
+
|
|
688
|
+
const namesUnder = (e: Expr): string[] =>
|
|
689
|
+
[...subterms(e)].filter((y): y is Extract<Expr, { k: 'var' }> => y.k === 'var').map((y) => y.name);
|
|
690
|
+
|
|
691
|
+
/** Does a READ land on a device register? Two readings, and neither is enough alone. The chain's
|
|
692
|
+
* ROOT is what places an access whose subscripts are not constant — `((struct E *)0x03003430)
|
|
693
|
+
* [a1].field_4` has no compile-time address at all — and a read with no root is unplaceable, so
|
|
694
|
+
* it bars. The WHOLE address is what places one whose subscripts are: `((s32 *)0x03FFFFF0)[8]`
|
|
695
|
+
* denotes 0x04000010, BG0HOFS, which the root alone reports as EWRAM. The residual is stated
|
|
696
|
+
* rather than hidden: a RUNTIME subscript can still carry an access from an out-of-window root
|
|
697
|
+
* into the window, and nothing here bounds it — the write side has no such gap because it
|
|
698
|
+
* resolves the whole address or refuses. */
|
|
699
|
+
const readsDevice = (r: Expr, window?: readonly [number, number]): boolean => {
|
|
700
|
+
const root = rootConst(r);
|
|
701
|
+
return root === null || inRange(root, window) || inRange(cellAddress(r), window);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
/** Can the region's writes change what a MEMORY read in the closed form sees? Only "no" when every
|
|
705
|
+
* write it evaluates goes to a constant address inside the declared device window, and every read
|
|
706
|
+
* lands outside it. See the file header, including the premise this does NOT establish (a device
|
|
707
|
+
* that writes memory itself: `deviceWritesMemory` below).
|
|
708
|
+
*
|
|
709
|
+
* MEMORY ONLY. The NAMES the closed form reads are `init-loop-var`'s question and
|
|
710
|
+
* `init-name-escapes`', and a closed form with no memory access still has names — which is why
|
|
711
|
+
* the early return below is not "nothing can change this". */
|
|
712
|
+
function movedReadAliasable(closed: Expr, evaluated: readonly Stmt[], window?: readonly [number, number]): boolean {
|
|
713
|
+
const reads = accessesIn(closed);
|
|
714
|
+
if (reads.length === 0) {
|
|
715
|
+
return false; // no access ⇒ no write can reach it; its NAMES are the two gates above
|
|
716
|
+
}
|
|
717
|
+
for (const s of allStmts(evaluated)) {
|
|
718
|
+
// a call or an unmodelled instruction may write anything
|
|
719
|
+
if (stmtExprs(s).some(exprHasEffect)) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
if (s.k === 'store' && !inRange(cellAddress(s.lval), window)) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return reads.some((r) => readsDevice(r, window));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Does the loop write a register the DEVICE answers by writing ordinary memory? The premise
|
|
730
|
+
* `movedReadAliasable` rests on covers the CPU's own stores and nothing else; a DMA trigger is a
|
|
731
|
+
* store whose effect is a write the C never spells. A store counts when its BYTE RANGE touches
|
|
732
|
+
* one of the target's declared ranges, so the 32-bit `DMA3CNT` write reaches the enable halfword
|
|
733
|
+
* four bytes into it. NO declared ranges ⇒ every device store counts, which is the conservative
|
|
734
|
+
* direction and what a target that has said nothing gets. */
|
|
735
|
+
function deviceWritesMemory(evaluated: readonly Stmt[], triggers?: readonly (readonly [number, number])[]): boolean {
|
|
736
|
+
for (const s of allStmts(evaluated)) {
|
|
737
|
+
if (s.k !== 'store' || s.lval.k !== 'index') {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const at = cellAddress(s.lval);
|
|
741
|
+
if (at === null) {
|
|
742
|
+
continue; // `movedReadAliasable` has already refused this loop
|
|
743
|
+
}
|
|
744
|
+
if (triggers === undefined) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
const end = at + s.lval.width;
|
|
748
|
+
if (triggers.some(([lo, hi]) => at < hi && end > lo)) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function* allStmts(stmts: readonly Stmt[]): Generator<Stmt> {
|
|
756
|
+
for (const s of stmts) {
|
|
757
|
+
yield s;
|
|
758
|
+
yield* allStmts(stmtChildren(s));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ── the pass ────────────────────────────────────────────────────────────────────────────────
|
|
763
|
+
|
|
764
|
+
/** the loop's counter step statement (a `for`'s `inc`, or the body's last statement) */
|
|
765
|
+
const counterStepStmt = (loop: Extract<Stmt, { k: 'while' | 'dowhile' | 'for' }>): Stmt | undefined =>
|
|
766
|
+
loop.k === 'for' ? loop.inc : loop.body[loop.body.length - 1];
|
|
767
|
+
|
|
768
|
+
/** the statements a loop evaluates outside its body — the parts a substitution must not touch */
|
|
769
|
+
const controlStmts = (loop: Extract<Stmt, { k: 'while' | 'dowhile' | 'for' }>): Stmt[] =>
|
|
770
|
+
loop.k === 'for' ? [loop.init, loop.inc] : [];
|
|
771
|
+
|
|
772
|
+
/** The statements a DEVICE armed in could still be writing memory from while the moved reads run:
|
|
773
|
+
* the loop, and the WHOLE prefix above it — deliberately wider than the motion region below,
|
|
774
|
+
* because a device armed anywhere before the reads happen keeps writing memory WHILE they happen,
|
|
775
|
+
* so a repeating transfer armed above the init is as asynchronous as one armed inside the loop. */
|
|
776
|
+
function armedPrefix(body: readonly Stmt[], loop: Stmt, li: number): Stmt[] {
|
|
777
|
+
return [...body.slice(0, li), loop];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** THE MOTION REGION: everything that runs between where the init stood and the reads that replace
|
|
781
|
+
* it. Both endpoints move — the init is DELETED, and the counter's start is what the substitution
|
|
782
|
+
* reads the closed form through — so the region opens at whichever of the two comes first and runs
|
|
783
|
+
* to the loop's last iteration. The loop enters WHOLE, so a walk over this region reaches its
|
|
784
|
+
* condition and a `for`'s own init and inc as well as its body. */
|
|
785
|
+
function motionRegion(body: readonly Stmt[], loop: Stmt, li: number, initIdx: number, startIdx: number): Stmt[] {
|
|
786
|
+
return [...body.slice(Math.min(initIdx, startIdx) + 1, li), loop];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** The `/unreduce` candidate. `sfn` is a fresh tree, the input left untouched; `needsProof` says
|
|
790
|
+
* the closed form re-reads memory over a loop whose device writes may THEMSELVES write memory
|
|
791
|
+
* (see the header), so rank.ts may publish it only at a byte-exact score. */
|
|
792
|
+
export interface UnreduceResult {
|
|
793
|
+
sfn: SFn;
|
|
794
|
+
needsProof: boolean;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** The `/unreduce` candidate, or null when no accumulator qualifies. `window` is the target's
|
|
798
|
+
* declared device-register range (TargetDescription.capabilities.deviceRegisters) — absent, the
|
|
799
|
+
* variation still fires on a closed form that reads no memory. `triggers` is
|
|
800
|
+
* `capabilities.deviceMemoryWriters`; absent, EVERY device store is treated as one. */
|
|
801
|
+
export function unreduceAccumulators(
|
|
802
|
+
sfn: SFn,
|
|
803
|
+
window?: readonly [number, number],
|
|
804
|
+
triggers?: readonly (readonly [number, number])[],
|
|
805
|
+
gates: readonly Gate<AccCtx>[] = UNREDUCE_GATES,
|
|
806
|
+
): UnreduceResult | null {
|
|
807
|
+
const body = [...sfn.body];
|
|
808
|
+
const vt: VarTypes = declaredTypes(sfn);
|
|
809
|
+
const deletedInits = new Set<Stmt>();
|
|
810
|
+
const deletedLocals = new Set<string>();
|
|
811
|
+
let changed = false;
|
|
812
|
+
let needsProof = false;
|
|
813
|
+
|
|
814
|
+
for (let li = 0; li < body.length; li++) {
|
|
815
|
+
const loop = body[li];
|
|
816
|
+
if (loop.k !== 'while' && loop.k !== 'dowhile' && loop.k !== 'for') {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
// the counter: one name stepped by a constant, whose start stands above the loop
|
|
820
|
+
const ctrStep = counterStepStmt(loop);
|
|
821
|
+
if (ctrStep === undefined || ctrStep.k !== 'assign') {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const ctr = ctrStep.name;
|
|
825
|
+
const dStep = stepOf(ctrStep, ctr);
|
|
826
|
+
const d = dStep === null ? null : foldConsts(dStep);
|
|
827
|
+
if (d === null || d.k !== 'const' || d.value === 0) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
const startStmt =
|
|
831
|
+
loop.k === 'for'
|
|
832
|
+
? loop.init
|
|
833
|
+
: [...sfn.body.slice(0, li)].reverse().find((s) => s.k === 'assign' && s.name === ctr);
|
|
834
|
+
if (startStmt === undefined || startStmt.k !== 'assign' || startStmt.name !== ctr) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const outside = [...sfn.body.slice(0, li), ...sfn.body.slice(li + 1)];
|
|
838
|
+
const startIdx = loop.k === 'for' ? li : sfn.body.indexOf(startStmt);
|
|
839
|
+
const armed = armedPrefix(sfn.body, loop, li);
|
|
840
|
+
const rewrites = new Map<string, Expr>();
|
|
841
|
+
for (const cand of sfn.locals) {
|
|
842
|
+
const initStmt = sfn.body.slice(0, li).find((s) => s.k === 'assign' && s.name === cand.name);
|
|
843
|
+
const stepIdx = loop.body.findIndex((s) => stepOf(s, cand.name) !== null);
|
|
844
|
+
if (initStmt === undefined || initStmt.k !== 'assign' || stepIdx < 0 || cand.name === ctr) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const k = foldConsts(stepOf(loop.body[stepIdx], cand.name)!);
|
|
848
|
+
const closed = relate(initStmt.value, startStmt.value, ctr, k, d.value);
|
|
849
|
+
const initIdx = sfn.body.indexOf(initStmt);
|
|
850
|
+
const evaluated = motionRegion(sfn.body, loop, li, initIdx, startIdx);
|
|
851
|
+
const ctrLocal = sfn.locals.find((l) => l.name === ctr);
|
|
852
|
+
// The units the accumulator's step counts in, against the units the closed form's `+` would
|
|
853
|
+
// scale by. Both sides must be KNOWN and equal — see `stride-units`.
|
|
854
|
+
const accScale = arithScale(cand.type);
|
|
855
|
+
const initScale = arithScale(exprCType(initStmt.value, vt));
|
|
856
|
+
const ctx: AccCtx = {
|
|
857
|
+
unitsDisagree: accScale === null || initScale === null || accScale !== initScale,
|
|
858
|
+
assigns: assignCount(sfn.body, cand.name),
|
|
859
|
+
addrTaken: addrTakenIn(sfn.body, cand.name),
|
|
860
|
+
pinned: declarationPins(cand),
|
|
861
|
+
liveOutside: mentions(
|
|
862
|
+
outside.filter((s) => s !== initStmt),
|
|
863
|
+
cand.name,
|
|
864
|
+
),
|
|
865
|
+
readAtOrBelowStep:
|
|
866
|
+
mentions(loop.body.slice(stepIdx + 1), cand.name) ||
|
|
867
|
+
mentionsIn(k, cand.name) ||
|
|
868
|
+
mentionsIn(loop.cond, cand.name) ||
|
|
869
|
+
mentions(controlStmts(loop), cand.name),
|
|
870
|
+
counterAssigns: assignCount(sfn.body, ctr),
|
|
871
|
+
counterAddrTaken: addrTakenIn(sfn.body, ctr),
|
|
872
|
+
counterVolatile: ctrLocal?.volatile === true || ctrLocal?.pointeeVolatile === true,
|
|
873
|
+
hasContinue: hasContinueIn(loop.body),
|
|
874
|
+
declined: 'ok' in closed ? null : closed.declined,
|
|
875
|
+
// `evaluated`, not `loop.body`: a `for`'s counter is stepped in `loop.inc`
|
|
876
|
+
initLoopVar: [...namesUnder(initStmt.value)].some((n) => assignCount(evaluated, n) > 0),
|
|
877
|
+
initNameEscapes: [...namesUnder(initStmt.value)].some((n) => addrTakenIn(sfn.body, n)),
|
|
878
|
+
// read off the ORIGINAL init, not the substituted form: the init STATEMENT is deleted, so
|
|
879
|
+
// an effect inside the counter-start subterm the substitution replaces would be dropped
|
|
880
|
+
// rather than moved — one fewer execution, which no gate reading `closed` could see.
|
|
881
|
+
movedEffect: exprHasEffect(initStmt.value),
|
|
882
|
+
movedVolatile: exprReadsVolatile(initStmt.value, sfn),
|
|
883
|
+
movedAliasable: movedReadAliasable(initStmt.value, evaluated, window),
|
|
884
|
+
};
|
|
885
|
+
if (firstRejection(gates, ctx) !== null) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
// AND THE CLOSED FORM ITSELF, which the gates alone do not establish: five of them reject a
|
|
889
|
+
// reason `relate` declined for, so only the FULL table implies one exists. `gates.ts`'s
|
|
890
|
+
// differential ablation drops an entry and re-runs this pass, and with one of those five
|
|
891
|
+
// gone there is nothing to substitute while the init statement and the declaration are
|
|
892
|
+
// deleted regardless — C naming a variable that is no longer there, with no marker on it.
|
|
893
|
+
if (!('ok' in closed)) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
// The gates have placed every write the C performs; what they cannot place is a write the
|
|
897
|
+
// DEVICE performs in answer to one. A moved read over such a loop is offered under PROOF.
|
|
898
|
+
if (accessesIn(initStmt.value).length > 0 && deviceWritesMemory(armed, triggers)) {
|
|
899
|
+
needsProof = true;
|
|
900
|
+
}
|
|
901
|
+
rewrites.set(cand.name, closed.ok);
|
|
902
|
+
deletedInits.add(initStmt);
|
|
903
|
+
deletedLocals.add(cand.name);
|
|
904
|
+
}
|
|
905
|
+
if (rewrites.size === 0) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
changed = true;
|
|
909
|
+
const sub = (e: Expr): Expr => {
|
|
910
|
+
if (e.k === 'var') {
|
|
911
|
+
const hit = rewrites.get(e.name);
|
|
912
|
+
if (hit !== undefined) {
|
|
913
|
+
return clone(hit); // a FRESH node per use — identity-keyed rules downstream read it
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return mapExprChildren(e, sub);
|
|
917
|
+
};
|
|
918
|
+
const strip = (stmts: readonly Stmt[]): Stmt[] =>
|
|
919
|
+
stmts.filter((s) => !(s.k === 'assign' && rewrites.has(s.name))).map((s) => mapStmts(s, sub, strip));
|
|
920
|
+
body[li] = { ...loop, body: strip(loop.body) } as Stmt;
|
|
921
|
+
}
|
|
922
|
+
if (!changed) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
sfn: {
|
|
927
|
+
...sfn,
|
|
928
|
+
locals: sfn.locals.filter((l) => !deletedLocals.has(l.name)),
|
|
929
|
+
body: body.filter((s) => !deletedInits.has(s)),
|
|
930
|
+
},
|
|
931
|
+
needsProof,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const clone = (e: Expr): Expr => mapExprChildren({ ...e }, clone);
|
|
936
|
+
|
|
937
|
+
/** map a statement's own expressions and its nested lists in one step */
|
|
938
|
+
function mapStmts(s: Stmt, f: (e: Expr) => Expr, list: (l: readonly Stmt[]) => Stmt[]): Stmt {
|
|
939
|
+
switch (s.k) {
|
|
940
|
+
case 'assign':
|
|
941
|
+
return { ...s, value: f(s.value) };
|
|
942
|
+
case 'store':
|
|
943
|
+
return { ...s, lval: f(s.lval), value: f(s.value) };
|
|
944
|
+
case 'exprstmt':
|
|
945
|
+
return { ...s, value: f(s.value) };
|
|
946
|
+
case 'return':
|
|
947
|
+
return s.value === undefined ? s : { ...s, value: f(s.value) };
|
|
948
|
+
case 'if':
|
|
949
|
+
return { ...s, cond: f(s.cond), then: list(s.then), else: list(s.else) };
|
|
950
|
+
case 'while':
|
|
951
|
+
case 'dowhile':
|
|
952
|
+
return { ...s, cond: f(s.cond), body: list(s.body) };
|
|
953
|
+
case 'for':
|
|
954
|
+
return {
|
|
955
|
+
...s,
|
|
956
|
+
cond: f(s.cond),
|
|
957
|
+
init: mapStmts(s.init, f, list),
|
|
958
|
+
inc: mapStmts(s.inc, f, list),
|
|
959
|
+
body: list(s.body),
|
|
960
|
+
};
|
|
961
|
+
case 'switch':
|
|
962
|
+
return {
|
|
963
|
+
...s,
|
|
964
|
+
scrutinee: f(s.scrutinee),
|
|
965
|
+
cases: s.cases.map((c) => ({ ...c, body: list(c.body) })),
|
|
966
|
+
...(s.default ? { default: list(s.default) } : {}),
|
|
967
|
+
};
|
|
968
|
+
default:
|
|
969
|
+
return s;
|
|
970
|
+
}
|
|
971
|
+
}
|