@asmlift/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- 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 +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/l3/reindex.ts
CHANGED
|
@@ -20,27 +20,103 @@
|
|
|
20
20
|
// for k 0), the bound → `i <op> N`, the step → `i = i + 1`. Everything else declines — the
|
|
21
21
|
// function keeps only its walk spelling, and no candidate is emitted.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
23
|
+
// v2 SCOPE — the GUARDED COUNTDOWN, the shape agbcc actually emits for `for(i=0;i<n;i++) a[i]`:
|
|
24
|
+
//
|
|
25
|
+
// if (0 >= n) { C } else { C ⊎ { p = B; k = n }; do { BODY; p += 1; k -= 1 } while (k != 0) }
|
|
26
|
+
//
|
|
27
|
+
// re-spells as `C; p = B; for (i = 0; i < n; i++) BODY[p[c] → p[i + c]]`. The `for` tests at top,
|
|
28
|
+
// so the guard disappears (it IS the for-condition at entry), and the skip arm C disappears with
|
|
29
|
+
// it (its statements are the loop-preceding subset of the else arm, now unconditionally hoisted).
|
|
30
|
+
// The walk pointers KEEP their init and lose their step — gcc folds a loop-invariant pointer
|
|
31
|
+
// local into addressing, so `p = B; … p[i]` and `B[i]` compile identically, and keeping the
|
|
32
|
+
// local is what lets the `/volatile` lever qualify a numeric B. Several walk pointers share the
|
|
33
|
+
// one counter (`dotprod`'s a/b pair). Its OWN rules, on top of the shared table below —
|
|
34
|
+
// • the guard tests THE SAME var the counter is initialised from, against 0, in the sense that
|
|
35
|
+
// skips the loop; the do-while exit is exactly `k != 0`;
|
|
36
|
+
// • the skip arm's statements equal, in order, the else arm's loop-preceding statements minus
|
|
37
|
+
// the induction inits — anything left over means the arms are not the same computation;
|
|
38
|
+
// • n is never assigned in the function (a moving bound has no single trip count).
|
|
39
|
+
// Everything else declines — the function keeps its countdown spelling, no candidate emitted.
|
|
40
|
+
//
|
|
41
|
+
// THE SHARED ADMISSION TABLE (`COUNTDOWN_GATES`) — the rules v2 and v4 both answer, as data, so
|
|
42
|
+
// "is this rule load-bearing?" is an ablation a test runs rather than an argument a reader
|
|
43
|
+
// audits. Each entry's `why` is its label; the argument is here:
|
|
44
|
+
// • the counter is INTEGER-typed. A pointer's `k - 1` strides its element size, so the trip
|
|
45
|
+
// count the rewrite computes would be a different loop's;
|
|
46
|
+
// • the counter is a DECLARED local or param. The rewrite deletes its init and its decrement,
|
|
47
|
+
// licensed by the four-roles rule proving nothing in THIS function reads it afterwards — not
|
|
48
|
+
// a property a global has, whose final value every other caller, ISR and translation unit
|
|
49
|
+
// observes. structure.ts spells a store to a bare scalar global as an `assign` like any
|
|
50
|
+
// other, so a global counter arrives here looking exactly like a local one;
|
|
51
|
+
// • neither the counter nor any walk pointer is declared `volatile`. Deleting the init, the
|
|
52
|
+
// decrement or a `p += 1` deletes accesses another agent is expected to see — a DMA-published
|
|
53
|
+
// frame slot (structure.ts stamps those) is the live shape;
|
|
54
|
+
// • the counter appears in EXACTLY its four roles (init write, decrement write + read, exit
|
|
55
|
+
// read), counted over the whole function; any other use — a leftover's read, the body's, a
|
|
56
|
+
// nested loop's, or an ESCAPED ADDRESS (`&k`, which the mention count sees) — would survive
|
|
57
|
+
// the deletion;
|
|
58
|
+
// • no walk pointer is stepped TWICE in one body — two `p += 1` stride 2, which `p[i]` does
|
|
59
|
+
// not. (That the tail holds a decrement and at least one step is shape recognition, not an
|
|
60
|
+
// admission rule: without them there is no countdown here to refuse.);
|
|
61
|
+
// • each walk pointer has an init ahead of the loop whose base the rewrite can leave standing:
|
|
62
|
+
// a var, or a rematerializable address (see `rematerializableAddress`);
|
|
63
|
+
// • every p is mentioned only inside the loop shape, and never by a leftover statement — a
|
|
64
|
+
// leftover outlives the deleted step, and its skip-arm twin reads a pointer that path never
|
|
65
|
+
// initialised;
|
|
66
|
+
// • every deref of a walk pointer reads its own element size (a `*(u8 *)p` over an `s32 *`
|
|
67
|
+
// walk strides 4 as a walk but 1 re-indexed — the v1 stride rule, same reason);
|
|
68
|
+
// • BODY has no `break`/`continue` targeting this loop — the original steps sat in the body's
|
|
69
|
+
// tail, which a `continue` skips, but a `for`'s inc runs: the two shapes genuinely differ.
|
|
70
|
+
// Past the table, the counter's DECLARATION is retired with its writes: nothing mentions it any
|
|
71
|
+
// more, and a declared-but-untouched local reads as the deliberate `uninit` slot it is not.
|
|
72
|
+
//
|
|
73
|
+
// v3 SCOPE — the up-counting BYTE walk with an EXPRESSION base (`p = (u8 *)(EXPR)` ahead of a
|
|
74
|
+
// counter-carried do-while): full rules at `tryExprWalk` below.
|
|
75
|
+
//
|
|
76
|
+
// v4 SCOPE — the UNGUARDED countdown, the shape agbcc emits for `for(i=0;i<C;i++) a[i]` with a
|
|
77
|
+
// LITERAL bound. A constant trip count needs no zero-trip guard, and the exit test agbcc writes
|
|
78
|
+
// for it is `>= 0` rather than v2's `!= 0`:
|
|
79
|
+
//
|
|
80
|
+
// p = B; k = C; do { BODY; p += 1; k -= 1 } while (k >= 0)
|
|
81
|
+
//
|
|
82
|
+
// re-spells as `p = B; for (i = 0; i < C + 1; i++) BODY[p[c] → p[i + c]]` — k counts C…0, so the
|
|
83
|
+
// body runs C + 1 times. Everything downstream of the counter's init is v2's (respellCountdown);
|
|
84
|
+
// what this shape adds is its own two rules —
|
|
85
|
+
// • the counter is declared SIGNED. `k >= 0` never fails for an unsigned k, so an unsigned
|
|
86
|
+
// declaration describes a loop that does not terminate, and `C + 1` would be a fiction;
|
|
87
|
+
// • C is a constant in [0, S32_MAX). Below zero `k = -1` runs the body once rather than
|
|
88
|
+
// `C + 1 = 0` times; at the top C + 1 wraps negative and the `for` runs zero times.
|
|
89
|
+
// The induction inits are read out of the statements PRECEDING the do-while in its own list
|
|
90
|
+
// rather than out of a guard arm, and the counter's init is the only one the rewrite deletes.
|
|
31
91
|
import { IrType, T } from '../ir/types';
|
|
32
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
Expr,
|
|
94
|
+
SFn,
|
|
95
|
+
Stmt,
|
|
96
|
+
mapExprChildren,
|
|
97
|
+
mapStmtExprs,
|
|
98
|
+
rematerializableAddress,
|
|
99
|
+
stmtChildren,
|
|
100
|
+
stmtExprs,
|
|
101
|
+
} from './ast';
|
|
102
|
+
import { type Gate, firstRejection } from './gates';
|
|
103
|
+
import { takenNames } from './hoist';
|
|
104
|
+
import { nameStorage } from './storage';
|
|
105
|
+
import { type VarTypes, declaredTypes } from './typing';
|
|
33
106
|
|
|
34
107
|
interface WalkLoop {
|
|
35
108
|
p: string; // the pointer induction var
|
|
36
109
|
base: string; // the var `p` was initialised from
|
|
37
110
|
}
|
|
38
111
|
|
|
39
|
-
/** Total mentions of `name` across a statement list (reads, writes, everywhere).
|
|
112
|
+
/** Total mentions of `name` across a statement list (reads, writes, everywhere). `addr` counts:
|
|
113
|
+
* `&x` is how a frame local's address renders (structure.ts laddrName), and an escaped address
|
|
114
|
+
* is a read of the object by whatever holds it — the accounting these recognizers rest on is
|
|
115
|
+
* "nothing else in the function touches this name", which a var-only count cannot say. */
|
|
40
116
|
function countMentions(stmts: Stmt[], name: string): number {
|
|
41
117
|
let n = 0;
|
|
42
118
|
const inExpr = (e: Expr): void => {
|
|
43
|
-
if (e.k === 'var' && e.name === name) {
|
|
119
|
+
if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
|
|
44
120
|
n++;
|
|
45
121
|
}
|
|
46
122
|
mapExprChildren(e, (c) => {
|
|
@@ -53,17 +129,7 @@ function countMentions(stmts: Stmt[], name: string): number {
|
|
|
53
129
|
n++;
|
|
54
130
|
}
|
|
55
131
|
stmtExprs(s).forEach(inExpr);
|
|
56
|
-
|
|
57
|
-
s.k === 'if'
|
|
58
|
-
? [...s.then, ...s.else]
|
|
59
|
-
: s.k === 'while' || s.k === 'dowhile'
|
|
60
|
-
? s.body
|
|
61
|
-
: s.k === 'for'
|
|
62
|
-
? [s.init, s.inc, ...s.body]
|
|
63
|
-
: s.k === 'switch'
|
|
64
|
-
? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
|
|
65
|
-
: [];
|
|
66
|
-
kids.forEach(inStmt);
|
|
132
|
+
stmtChildren(s).forEach(inStmt);
|
|
67
133
|
};
|
|
68
134
|
stmts.forEach(inStmt);
|
|
69
135
|
return n;
|
|
@@ -96,25 +162,15 @@ function derefWidths(stmts: Stmt[], p: string): number[] {
|
|
|
96
162
|
};
|
|
97
163
|
const inStmt = (s: Stmt): void => {
|
|
98
164
|
stmtExprs(s).forEach(inExpr);
|
|
99
|
-
|
|
100
|
-
s.k === 'if'
|
|
101
|
-
? [...s.then, ...s.else]
|
|
102
|
-
: s.k === 'while' || s.k === 'dowhile'
|
|
103
|
-
? s.body
|
|
104
|
-
: s.k === 'for'
|
|
105
|
-
? [s.init, s.inc, ...s.body]
|
|
106
|
-
: s.k === 'switch'
|
|
107
|
-
? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
|
|
108
|
-
: [];
|
|
109
|
-
kids.forEach(inStmt);
|
|
165
|
+
stmtChildren(s).forEach(inStmt);
|
|
110
166
|
};
|
|
111
167
|
stmts.forEach(inStmt);
|
|
112
168
|
return out;
|
|
113
169
|
}
|
|
114
170
|
|
|
115
|
-
/** Does `e` mention
|
|
171
|
+
/** Does `e` mention `name` anywhere — as a value or as an escaped address (see countMentions)? */
|
|
116
172
|
function mentionsVar(e: Expr, name: string): boolean {
|
|
117
|
-
if (e.k === 'var') {
|
|
173
|
+
if (e.k === 'var' || e.k === 'addr') {
|
|
118
174
|
return e.name === name;
|
|
119
175
|
}
|
|
120
176
|
let found = false;
|
|
@@ -125,26 +181,151 @@ function mentionsVar(e: Expr, name: string): boolean {
|
|
|
125
181
|
return found;
|
|
126
182
|
}
|
|
127
183
|
|
|
184
|
+
/** Is `name` the target of any assignment, at any depth? */
|
|
185
|
+
function stmtAssigns(s: Stmt, name: string): boolean {
|
|
186
|
+
if (s.k === 'assign' && s.name === name) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return stmtChildren(s).some((x) => stmtAssigns(x, name));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** One countdown loop as the shared admission rules read it: every fact collected before any rule
|
|
193
|
+
* runs. What is NOT here is the shape recognition — a body tail holding a decrement and at least
|
|
194
|
+
* one pointer step — because without it there is no countdown to admit or refuse. */
|
|
195
|
+
export interface CountdownCtx {
|
|
196
|
+
/** the counter's declaration, as the tree spells it */
|
|
197
|
+
kIsPointer: boolean;
|
|
198
|
+
kIsDeclared: boolean;
|
|
199
|
+
kIsVolatile: boolean;
|
|
200
|
+
/** a walk pointer stepped twice in one body */
|
|
201
|
+
dupStep: boolean;
|
|
202
|
+
/** walk pointers declared `volatile` */
|
|
203
|
+
volatileWalks: readonly string[];
|
|
204
|
+
/** the body left after the induction tail carries a `break`/`continue` for this loop */
|
|
205
|
+
coreHasExit: boolean;
|
|
206
|
+
/** walk pointers with no init ahead of the loop, or one whose base the rewrite cannot keep */
|
|
207
|
+
badBases: readonly string[];
|
|
208
|
+
/** total mentions of the counter across the WHOLE function */
|
|
209
|
+
kMentions: number;
|
|
210
|
+
/** walk pointers mentioned outside the loop shape */
|
|
211
|
+
unconfined: readonly string[];
|
|
212
|
+
/** walk pointers whose derefs disagree with their own element size */
|
|
213
|
+
badStrides: readonly string[];
|
|
214
|
+
/** statements ahead of the loop that survive it and still mention a walk pointer */
|
|
215
|
+
leakyLeftovers: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The admission rules v2 and v4 share, as data. The argument for each lives in the file header;
|
|
219
|
+
* `why` is the label. */
|
|
220
|
+
export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
221
|
+
{
|
|
222
|
+
id: 'pointer-counter',
|
|
223
|
+
why: 'C pointer arithmetic strides the pointee, so `k - 1` on a pointer counts elements, not iterations',
|
|
224
|
+
sound: true,
|
|
225
|
+
guardedBy: 'reindex.test.ts: a pointer-typed counter declines',
|
|
226
|
+
rejects: (c) => c.kIsPointer,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'global-counter',
|
|
230
|
+
why: 'a global counter’s final value is observable to every other caller, ISR and translation unit',
|
|
231
|
+
sound: true,
|
|
232
|
+
guardedBy: 'reindex.test.ts: the counter may not be a GLOBAL',
|
|
233
|
+
rejects: (c) => !c.kIsDeclared,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'volatile-counter',
|
|
237
|
+
why: 'the rewrite deletes the counter’s init and decrement, which a volatile object’s reader still expects',
|
|
238
|
+
sound: true,
|
|
239
|
+
guardedBy: 'reindex.test.ts: a volatile counter declines',
|
|
240
|
+
rejects: (c) => c.kIsVolatile,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'volatile-walk',
|
|
244
|
+
why: 're-basing the derefs off a plain local turns N accesses to volatile data into N plain ones',
|
|
245
|
+
sound: true,
|
|
246
|
+
guardedBy: 'reindex.test.ts: a volatile walk pointer declines',
|
|
247
|
+
rejects: (c) => c.volatileWalks.length > 0,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'two-steps-one-pointer',
|
|
251
|
+
why: 'a pointer stepped twice per iteration strides 2, which `p[i]` does not',
|
|
252
|
+
sound: true,
|
|
253
|
+
guardedBy: 'reindex.test.ts: a pointer stepped twice in one body declines',
|
|
254
|
+
rejects: (c) => c.dupStep,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: 'body-exit',
|
|
258
|
+
why: 'the steps sat in the body tail, which a `continue` skips and a `for`’s inc does not',
|
|
259
|
+
sound: true,
|
|
260
|
+
guardedBy: 'reindex.test.ts: a `break` in the body declines',
|
|
261
|
+
rejects: (c) => c.coreHasExit,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'walk-base',
|
|
265
|
+
why: 'the kept init must be a value the rewrite can leave standing — a var, or a rematerializable address',
|
|
266
|
+
sound: true,
|
|
267
|
+
guardedBy: 'reindex.test.ts: a walk pointer with no init ahead of the loop declines',
|
|
268
|
+
rejects: (c) => c.badBases.length > 0,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'counter-roles',
|
|
272
|
+
why: 'exactly four mentions — init write, decrement write and read, exit read — are what the rewrite deletes',
|
|
273
|
+
sound: true,
|
|
274
|
+
guardedBy: 'reindex.test.ts: a fifth mention of the counter declines',
|
|
275
|
+
rejects: (c) => c.kMentions !== 4,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'walk-confined',
|
|
279
|
+
why: 'a mention outside the loop shape reads a pointer whose step the rewrite deleted',
|
|
280
|
+
sound: true,
|
|
281
|
+
guardedBy: 'reindex.test.ts: a walk pointer read after the loop declines',
|
|
282
|
+
rejects: (c) => c.unconfined.length > 0,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'walk-stride',
|
|
286
|
+
why: 'a deref reading a width other than the pointee’s makes walk and index forms read different addresses',
|
|
287
|
+
sound: true,
|
|
288
|
+
guardedBy: 'reindex.test.ts: a byte deref of a word walk declines',
|
|
289
|
+
rejects: (c) => c.badStrides.length > 0,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: 'leftover-walk',
|
|
293
|
+
why: 'a leftover outlives the deleted step, and its skip-arm twin reads a pointer that path never set',
|
|
294
|
+
sound: true,
|
|
295
|
+
guardedBy: 'reindex.test.ts: a leftover mentioning a walk pointer declines',
|
|
296
|
+
rejects: (c) => c.leakyLeftovers > 0,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
/** A `break`/`continue` that would target the ENCLOSING loop: found at if/switch depth, but not
|
|
301
|
+
* inside a nested loop (whose own exits target itself). */
|
|
302
|
+
function hasLoopExit(stmts: Stmt[]): boolean {
|
|
303
|
+
return stmts.some((s) => {
|
|
304
|
+
switch (s.k) {
|
|
305
|
+
case 'break':
|
|
306
|
+
case 'continue':
|
|
307
|
+
return true;
|
|
308
|
+
case 'if':
|
|
309
|
+
return hasLoopExit(s.then) || hasLoopExit(s.else);
|
|
310
|
+
case 'switch':
|
|
311
|
+
return s.cases.some((c) => hasLoopExit(c.body)) || hasLoopExit(s.default ?? []);
|
|
312
|
+
default:
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
128
318
|
function stmtMentions(s: Stmt, name: string): boolean {
|
|
129
319
|
if (s.k === 'assign' && s.name === name) {
|
|
130
320
|
return true;
|
|
131
321
|
}
|
|
132
|
-
const kids
|
|
133
|
-
s.k === 'if'
|
|
134
|
-
? [...s.then, ...s.else]
|
|
135
|
-
: s.k === 'while' || s.k === 'dowhile'
|
|
136
|
-
? s.body
|
|
137
|
-
: s.k === 'for'
|
|
138
|
-
? [s.init, s.inc, ...s.body]
|
|
139
|
-
: s.k === 'switch'
|
|
140
|
-
? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
|
|
141
|
-
: [];
|
|
322
|
+
const kids = stmtChildren(s);
|
|
142
323
|
return stmtExprs(s).some((e) => mentionsVar(e, name)) || kids.some((k) => stmtMentions(k, name));
|
|
143
324
|
}
|
|
144
325
|
|
|
145
|
-
/** `
|
|
146
|
-
function
|
|
147
|
-
if (s.k !== 'assign'
|
|
326
|
+
/** `x = x + 1` on ANY variable — the assigned name, or null. */
|
|
327
|
+
function isUnitIncrement(s: Stmt): string | null {
|
|
328
|
+
if (s.k !== 'assign') {
|
|
148
329
|
return null;
|
|
149
330
|
}
|
|
150
331
|
const v = s.value;
|
|
@@ -153,6 +334,20 @@ function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
|
|
|
153
334
|
return ok ? s.name : null;
|
|
154
335
|
}
|
|
155
336
|
|
|
337
|
+
/** `assign(p, p + 1)` on a pointer-typed `p` — the walk's step; the assigned name, or null. */
|
|
338
|
+
function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
|
|
339
|
+
const n = isUnitIncrement(s);
|
|
340
|
+
return n !== null && ptrVars.has(n) ? n : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** ADVERSARIALLY LEARNED (reproduced as an uninitialized-read escape): the walk pointer must be
|
|
344
|
+
* mentioned NOWHERE in the function outside its init and its loop — counted GLOBALLY, because a
|
|
345
|
+
* suffix-only scan missed reads after an ENCLOSING construct, leaving the deleted init's var
|
|
346
|
+
* read uninitialized. One spelling for every recognizer. */
|
|
347
|
+
function confinedToWalk(fnBody: Stmt[], name: string, mentionsOutsideLoop: number, loop: Stmt): boolean {
|
|
348
|
+
return countMentions(fnBody, name) === mentionsOutsideLoop + countMentions([loop], name);
|
|
349
|
+
}
|
|
350
|
+
|
|
156
351
|
/** Rewrite every deref of `p` into an indexed access off `base`, and every OTHER mention of `p`
|
|
157
352
|
* fails the walk (returns null): `p[k]` → `base[i + k]` (`base[i]` for k 0). */
|
|
158
353
|
function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
@@ -171,9 +366,14 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
171
366
|
e.idx.k === 'const' && e.idx.value === 0
|
|
172
367
|
? { k: 'var', name: iv }
|
|
173
368
|
: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
|
|
174
|
-
// NOTE: this rebuilds the node from parts, so any field not named here is DROPPED
|
|
175
|
-
//
|
|
176
|
-
// is
|
|
369
|
+
// NOTE: this rebuilds the node from parts, so any field not named here is DROPPED — and the
|
|
370
|
+
// `index` node has exactly three optional ones (ast.ts): `lead`, `operandOff`, `baseOrdered`.
|
|
371
|
+
// `lead` is declined above (the deref side); it cannot arrive on the base side either, since
|
|
372
|
+
// `walk.base` is a local pointer and structuring only ever puts `lead` on an array GLOBAL's
|
|
373
|
+
// own name. Dropping the other two is right rather than merely harmless: `baseOrdered` is
|
|
374
|
+
// stamped per SYMBOL on an order-licensed GLOBAL, which a local var base never is, and
|
|
375
|
+
// `operandOff`'s readers (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript,
|
|
376
|
+
// which the rewritten `i`/`i + k` index never is.
|
|
177
377
|
return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
|
|
178
378
|
}
|
|
179
379
|
let failed = false;
|
|
@@ -296,11 +496,181 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
|
|
|
296
496
|
}
|
|
297
497
|
}
|
|
298
498
|
|
|
499
|
+
// ── v3: the up-counting BYTE walk with an EXPRESSION base ────────────────────────────────────
|
|
500
|
+
//
|
|
501
|
+
// p = (u8 *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
|
|
502
|
+
//
|
|
503
|
+
// The loop already carries its own integer counter, so the walk pointer is pure strength
|
|
504
|
+
// reduction: deleting its init and step and spelling the deref as BASE[i + REST] — the sole
|
|
505
|
+
// bare-var addend of EXPR as the index base, the counter FIRST in the index sum — is the
|
|
506
|
+
// indexed source the compiler reduces back. v3 SCOPE (decline over approximate):
|
|
507
|
+
// • `p` is a `u8 *` (byte walk: the index and the byte arithmetic agree by construction —
|
|
508
|
+
// a wider element would need the REST addends rescaled from bytes to elements);
|
|
509
|
+
// • the init `p = EXPR` immediately precedes the `dowhile`; EXPR (casts stripped) flattens
|
|
510
|
+
// over `+` into exactly ONE bare-var addend (the base — itself not a non-byte pointer, or
|
|
511
|
+
// `base[i]` would stride its element) plus pure invariant addends (var/const/mul/shl trees
|
|
512
|
+
// whose vars the body never assigns);
|
|
513
|
+
// • the body's statements include exactly one `p = p + 1` and one `i = i + 1` with integer
|
|
514
|
+
// `i` read by the do-while condition; `p` is mentioned nowhere else in the function beyond
|
|
515
|
+
// its init, its step, and derefs AT OFFSET 0 inside this body, and not by the condition.
|
|
516
|
+
function tryExprWalk(
|
|
517
|
+
prev2: Stmt | undefined,
|
|
518
|
+
prev: Stmt | undefined,
|
|
519
|
+
dw: Stmt & { k: 'dowhile' },
|
|
520
|
+
ptrVars: Map<string, IrType>,
|
|
521
|
+
fnBody: Stmt[],
|
|
522
|
+
declTypes: VarTypes,
|
|
523
|
+
volatileLocals: ReadonlySet<string>,
|
|
524
|
+
): Stmt | null {
|
|
525
|
+
if (prev?.k !== 'assign' || !ptrVars.has(prev.name) || volatileLocals.has(prev.name)) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const p = prev.name;
|
|
529
|
+
const pT = ptrVars.get(p)!;
|
|
530
|
+
if (pT.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width !== 8) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (!derefWidths([dw], p).every((w) => w === 1)) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
// EXPR decomposition
|
|
537
|
+
let expr: Expr = prev.value;
|
|
538
|
+
while (expr.k === 'cast') {
|
|
539
|
+
expr = expr.e;
|
|
540
|
+
}
|
|
541
|
+
const addends: Expr[] = [];
|
|
542
|
+
const flatten = (e: Expr): void => {
|
|
543
|
+
if (e.k === 'bin' && e.op === '+') {
|
|
544
|
+
flatten(e.l);
|
|
545
|
+
flatten(e.r);
|
|
546
|
+
} else {
|
|
547
|
+
addends.push(e);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
flatten(expr);
|
|
551
|
+
const bare = addends.filter((a): a is Extract<Expr, { k: 'var' }> => a.k === 'var');
|
|
552
|
+
const rest = addends.filter((a) => a.k !== 'var');
|
|
553
|
+
if (bare.length !== 1) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const base = bare[0] as Extract<Expr, { k: 'var' }>;
|
|
557
|
+
// declTypes carries params, locals AND globals: a base declared nowhere has an unknowable C
|
|
558
|
+
// stride (its project declaration decides), and a wider pointer strides its element under [i]
|
|
559
|
+
// — both decline.
|
|
560
|
+
const baseT = declTypes(base.name);
|
|
561
|
+
if (baseT === undefined) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
if (baseT.kind === 'ptr' && !(baseT.to.kind === 'int' && baseT.to.width === 8)) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const pure = (e: Expr): boolean =>
|
|
568
|
+
e.k === 'var' || e.k === 'const'
|
|
569
|
+
? true
|
|
570
|
+
: e.k === 'bin' && (e.op === '*' || e.op === '+' || e.op === '<<')
|
|
571
|
+
? pure(e.l) && pure(e.r)
|
|
572
|
+
: e.k === 'cast'
|
|
573
|
+
? pure(e.e)
|
|
574
|
+
: false;
|
|
575
|
+
const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
|
|
576
|
+
const restVars = new Set<string>();
|
|
577
|
+
const collectVars = (e: Expr): void => {
|
|
578
|
+
if (e.k === 'var') {
|
|
579
|
+
restVars.add(e.name);
|
|
580
|
+
}
|
|
581
|
+
mapExprChildren(e, (c) => {
|
|
582
|
+
collectVars(c);
|
|
583
|
+
return c;
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
rest.forEach(collectVars);
|
|
587
|
+
restVars.add(base.name);
|
|
588
|
+
if (!rest.every(pure) || [...restVars].some((n) => assignedIn(dw.body, n))) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
// the steps and the counter
|
|
592
|
+
const isIncOf = (st: Stmt, name?: string): string | null => {
|
|
593
|
+
const n = isUnitIncrement(st);
|
|
594
|
+
return n !== null && (name === undefined || n === name) ? n : null;
|
|
595
|
+
};
|
|
596
|
+
// BOTH steps as the body's contiguous TAIL (either order): every deref then runs with `i`
|
|
597
|
+
// completed steps equal to `p`'s, which is what `BASE[i + REST]` states. A step anywhere
|
|
598
|
+
// earlier (the `*++p` walk, a deref straddling the step) reads a different address.
|
|
599
|
+
const tail = dw.body.slice(-2);
|
|
600
|
+
const tailIncs = tail.map((st) => isIncOf(st)).filter((n): n is string => n !== null);
|
|
601
|
+
if (tailIncs.length !== 2 || !tailIncs.includes(p)) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const iv = tailIncs.find((n) => n !== p);
|
|
605
|
+
if (iv === undefined) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
const pStepIdx = dw.body.length - 2 + tail.findIndex((st) => isIncOf(st, p) !== null);
|
|
609
|
+
const ivT = declTypes(iv);
|
|
610
|
+
if (ivT !== undefined && ivT.kind !== 'int') {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
if (!mentionsVar(dw.cond, iv) || mentionsVar(dw.cond, p)) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
// The counter STARTS AT ZERO (the adjacent `i = 0` init — `BASE[i + REST]` counts completed
|
|
617
|
+
// steps from the walk's own start) and its ONLY write anywhere in the body is the tail step.
|
|
618
|
+
if (prev2?.k !== 'assign' || prev2.name !== iv || prev2.value.k !== 'const' || prev2.value.value !== 0) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
if (dw.body.slice(0, -2).some((st) => stmtAssigns(st, iv)) || tail.filter((st) => isIncOf(st, iv)).length !== 1) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
if (volatileLocals.has(iv) || [...restVars].some((n) => volatileLocals.has(n))) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
// rewrite the derefs, then verify nothing of `p` survives
|
|
628
|
+
const idxOf = (): Expr => rest.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv });
|
|
629
|
+
let respelled = true;
|
|
630
|
+
const rewriteExpr = (e: Expr): Expr => {
|
|
631
|
+
const m = mapExprChildren(e, rewriteExpr);
|
|
632
|
+
if (m.k === 'index' && m.base.k === 'var' && m.base.name === p) {
|
|
633
|
+
if (m.idx.k !== 'const' || m.idx.value !== 0) {
|
|
634
|
+
respelled = false;
|
|
635
|
+
return m;
|
|
636
|
+
}
|
|
637
|
+
return { ...m, base: { k: 'var', name: base.name }, idx: idxOf() };
|
|
638
|
+
}
|
|
639
|
+
return m;
|
|
640
|
+
};
|
|
641
|
+
const body = dw.body.filter((_, k) => k !== pStepIdx).map((st) => mapStmtExprs(st, rewriteExpr));
|
|
642
|
+
const dw2: Stmt = { ...dw, body };
|
|
643
|
+
if (!respelled || !confinedToWalk(fnBody, p, countMentions([prev], p), dw)) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (countMentions([dw2], p) !== 0) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
return dw2;
|
|
650
|
+
}
|
|
651
|
+
|
|
299
652
|
/** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
|
|
300
653
|
* one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
|
|
301
|
-
* candidate only on non-null. Pure: never mutates the input SFn.
|
|
302
|
-
|
|
654
|
+
* candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
|
|
655
|
+
* of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
|
|
656
|
+
* set too, inertly: the volatile lever marks only declared locals), v2 the walk pointers
|
|
657
|
+
* themselves — the locals the /indexed/volatile product (rank.ts) narrows the volatile lever
|
|
658
|
+
* to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
|
|
659
|
+
* the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
|
|
660
|
+
* so a test can ablate one entry and re-run the real pass. */
|
|
661
|
+
export function reindexWalks(
|
|
662
|
+
sfn: SFn,
|
|
663
|
+
keptWalks?: Set<string>,
|
|
664
|
+
gates: readonly Gate<CountdownCtx>[] = COUNTDOWN_GATES,
|
|
665
|
+
): SFn | null {
|
|
303
666
|
const ptrVars = new Map<string, IrType>();
|
|
667
|
+
const declTypes = declaredTypes(sfn);
|
|
668
|
+
// BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
|
|
669
|
+
// POINTEE is volatile — which is the one the `/volatile` lever mints, and the one a walk carries.
|
|
670
|
+
const volatileLocals = new Set(
|
|
671
|
+
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
672
|
+
);
|
|
673
|
+
const storage = nameStorage(sfn);
|
|
304
674
|
for (const v of [...sfn.params, ...sfn.locals]) {
|
|
305
675
|
if (v.type.kind === 'ptr') {
|
|
306
676
|
ptrVars.set(v.name, v.type);
|
|
@@ -311,7 +681,7 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
311
681
|
}
|
|
312
682
|
|
|
313
683
|
let fired = 0;
|
|
314
|
-
|
|
684
|
+
const taken = takenNames(sfn);
|
|
315
685
|
const locals = [...sfn.locals];
|
|
316
686
|
|
|
317
687
|
// SOUNDNESS GATE (adversarially learned; every rule REPRODUCED as a wrong-bytes or crash
|
|
@@ -320,14 +690,11 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
320
690
|
// • base and p must be pointers of the SAME element size, and every deref of p must read
|
|
321
691
|
// exactly that size — otherwise the walk (strides p's pointee) and the indexed form
|
|
322
692
|
// (strides base's) read different addresses;
|
|
323
|
-
// •
|
|
324
|
-
|
|
325
|
-
// var read uninitialized. Counted globally: total mentions == init + loop mentions.
|
|
326
|
-
const soundWalk = (walk: WalkLoop, initMentions: number, loop: Stmt): boolean =>
|
|
693
|
+
// • confinedToWalk — the global mention accounting, shared with v3.
|
|
694
|
+
const soundWalk = (walk: WalkLoop, mentionsOutsideLoop: number, loop: Stmt): boolean =>
|
|
327
695
|
walk.p !== walk.base &&
|
|
328
|
-
strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base)
|
|
329
|
-
|
|
330
|
-
const paramType = (n: string): IrType | undefined => sfn.params.find((x) => x.name === n)?.type;
|
|
696
|
+
strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base), derefWidths([loop], walk.p)) &&
|
|
697
|
+
confinedToWalk(sfn.body, walk.p, mentionsOutsideLoop, loop);
|
|
331
698
|
|
|
332
699
|
// walk a statement LIST so the `while` shape can see its preceding init statement
|
|
333
700
|
const walkList = (stmts: Stmt[]): Stmt[] => {
|
|
@@ -343,15 +710,15 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
343
710
|
init.k === 'assign' &&
|
|
344
711
|
init.name === p &&
|
|
345
712
|
init.value.k === 'var' &&
|
|
346
|
-
// init
|
|
347
|
-
// part of the loop stmt itself, so count the whole `for` node) — see soundWalk
|
|
713
|
+
// a `for`'s init lives inside the loop node, so confinedToWalk already counts it
|
|
348
714
|
soundWalk({ p, base: init.value.name }, 0, s)
|
|
349
715
|
) {
|
|
350
716
|
const walk: WalkLoop = { p, base: init.value.name };
|
|
351
|
-
const iv =
|
|
717
|
+
const iv = nextIv();
|
|
352
718
|
const cond = reindexCond(s.cond, walk, iv);
|
|
353
719
|
const body = cond ? reindexStmts(s.body, walk, iv) : null;
|
|
354
720
|
if (cond && body) {
|
|
721
|
+
declareIv(iv);
|
|
355
722
|
out.push({
|
|
356
723
|
k: 'for',
|
|
357
724
|
init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
|
|
@@ -364,9 +731,9 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
364
731
|
body,
|
|
365
732
|
});
|
|
366
733
|
fired++;
|
|
734
|
+
keptWalks?.add(walk.base);
|
|
367
735
|
continue;
|
|
368
736
|
}
|
|
369
|
-
retireIv();
|
|
370
737
|
}
|
|
371
738
|
}
|
|
372
739
|
// `p = base; while (p < base + n) { …; p = p + 1; }`
|
|
@@ -382,10 +749,11 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
382
749
|
soundWalk({ p, base: prev.value.name }, 1, s)
|
|
383
750
|
) {
|
|
384
751
|
const walk: WalkLoop = { p, base: prev.value.name };
|
|
385
|
-
const iv =
|
|
752
|
+
const iv = nextIv();
|
|
386
753
|
const cond = reindexCond(s.cond, walk, iv);
|
|
387
754
|
const body = cond ? reindexStmts(s.body.slice(0, -1), walk, iv) : null;
|
|
388
755
|
if (cond && body) {
|
|
756
|
+
declareIv(iv);
|
|
389
757
|
out[out.length - 1] = { k: 'assign', name: iv, value: { k: 'const', value: 0 } };
|
|
390
758
|
out.push({
|
|
391
759
|
k: 'while',
|
|
@@ -400,9 +768,40 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
400
768
|
],
|
|
401
769
|
});
|
|
402
770
|
fired++;
|
|
771
|
+
keptWalks?.add(walk.base);
|
|
403
772
|
continue;
|
|
404
773
|
}
|
|
405
|
-
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// v3 — the up-counting byte walk with an expression base: `p = (u8 *)(EXPR)` immediately
|
|
777
|
+
// ahead of a counter-carried do-while (see tryExprWalk)
|
|
778
|
+
if (s.k === 'dowhile') {
|
|
779
|
+
const prev = out[out.length - 1];
|
|
780
|
+
const prev2 = out[out.length - 2];
|
|
781
|
+
const dw2 = tryExprWalk(prev2, prev, s, ptrVars, sfn.body, declTypes, volatileLocals);
|
|
782
|
+
if (dw2) {
|
|
783
|
+
out.pop(); // the walk init is subsumed by the indexed spelling
|
|
784
|
+
out.push(recurse(dw2));
|
|
785
|
+
fired++;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
// v4 — the unguarded constant-trip countdown (see the header)
|
|
789
|
+
const cd = respellConstCountdown(out, s);
|
|
790
|
+
if (cd) {
|
|
791
|
+
out.length = 0;
|
|
792
|
+
out.push(...cd.pre, cd.counted);
|
|
793
|
+
fired++;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// the guarded countdown (v2 — see the header): an `if` whose one arm is skip-copies C and
|
|
798
|
+
// whose other arm is C ⊎ inductions followed by the do-while
|
|
799
|
+
if (s.k === 'if') {
|
|
800
|
+
const r = respellGuardedCountdown(s);
|
|
801
|
+
if (r) {
|
|
802
|
+
out.push(...r);
|
|
803
|
+
fired++;
|
|
804
|
+
continue;
|
|
406
805
|
}
|
|
407
806
|
}
|
|
408
807
|
// recurse into compound statements without re-spelling them
|
|
@@ -431,19 +830,257 @@ export function reindexWalks(sfn: SFn): SFn | null {
|
|
|
431
830
|
}
|
|
432
831
|
};
|
|
433
832
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
833
|
+
/** The countdown machinery the guarded (v2) and the unguarded constant-trip (v4) shapes share:
|
|
834
|
+
* everything downstream of "which statement inits the counter, and what is the trip count".
|
|
835
|
+
* Recognizes the shape, collects what `gates` reads, and refuses on the first entry that
|
|
836
|
+
* rejects. `confineTo` names the statements every mention of the counter and of each walk
|
|
837
|
+
* pointer must live in, given the induction inits this found; `accept` is the caller's own
|
|
838
|
+
* admission. Returns the inits it consumed, `pre` minus them, and the counted `for` — or null
|
|
839
|
+
* (decline). */
|
|
840
|
+
function respellCountdown(
|
|
841
|
+
loop: Stmt & { k: 'dowhile' },
|
|
842
|
+
pre: Stmt[],
|
|
843
|
+
k: string,
|
|
844
|
+
kInit: Stmt & { k: 'assign' },
|
|
845
|
+
trip: Expr,
|
|
846
|
+
confineTo: (induction: Stmt[]) => Stmt[],
|
|
847
|
+
accept?: (leftovers: Stmt[]) => boolean,
|
|
848
|
+
): { inductionInits: Set<Stmt>; leftovers: Stmt[]; counted: Stmt & { k: 'for' } } | null {
|
|
849
|
+
// SHAPE RECOGNITION — the body's contiguous tail is `k -= 1` and one `p += 1` per walk
|
|
850
|
+
// pointer. Not an admission rule: without a decrement and a step there is no countdown here
|
|
851
|
+
// at all, and the facts the rules read are derived from what this scan found.
|
|
852
|
+
const isDec = (x: Stmt): boolean =>
|
|
853
|
+
x.k === 'assign' &&
|
|
854
|
+
x.name === k &&
|
|
855
|
+
x.value.k === 'bin' &&
|
|
856
|
+
x.value.op === '-' &&
|
|
857
|
+
x.value.l.k === 'var' &&
|
|
858
|
+
x.value.l.name === k &&
|
|
859
|
+
x.value.r.k === 'const' &&
|
|
860
|
+
x.value.r.value === 1;
|
|
861
|
+
const walks: string[] = [];
|
|
862
|
+
let cut = loop.body.length;
|
|
863
|
+
let sawDec = false;
|
|
864
|
+
let dupStep = false;
|
|
865
|
+
for (let i = loop.body.length - 1; i >= 0; i--) {
|
|
866
|
+
const x = loop.body[i];
|
|
867
|
+
const p = isUnitStep(x, ptrVars);
|
|
868
|
+
if (p !== null) {
|
|
869
|
+
dupStep ||= walks.includes(p);
|
|
870
|
+
walks.push(p);
|
|
871
|
+
cut = i;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (!sawDec && isDec(x)) {
|
|
875
|
+
sawDec = true;
|
|
876
|
+
cut = i;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
440
880
|
}
|
|
441
|
-
|
|
442
|
-
|
|
881
|
+
if (!sawDec || walks.length === 0) {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
const bodyCore = loop.body.slice(0, cut);
|
|
885
|
+
const inits = new Map<string, Stmt & { k: 'assign' }>();
|
|
886
|
+
const badBases: string[] = [];
|
|
887
|
+
for (const p of walks) {
|
|
888
|
+
const init = pre.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === p);
|
|
889
|
+
const bOk =
|
|
890
|
+
init &&
|
|
891
|
+
(init.value.k === 'var' ? init.value.name !== p && init.value.name !== k : rematerializableAddress(init.value));
|
|
892
|
+
if (bOk) {
|
|
893
|
+
inits.set(p, init);
|
|
894
|
+
} else {
|
|
895
|
+
badBases.push(p);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const shape = confineTo([kInit, ...inits.values(), loop]);
|
|
899
|
+
const inductionInits = new Set<Stmt>([kInit, ...inits.values()]);
|
|
900
|
+
const leftovers = pre.filter((x) => !inductionInits.has(x));
|
|
901
|
+
const elemSize = (t: IrType | undefined): number =>
|
|
902
|
+
t?.kind === 'ptr' ? (t.to.kind === 'int' ? t.to.width / 8 : t.to.kind === 'ptr' ? 4 : 0) : 0;
|
|
903
|
+
const ctx: CountdownCtx = {
|
|
904
|
+
kIsPointer: ptrVars.has(k),
|
|
905
|
+
kIsDeclared: storage.get(k) === 'local' || storage.get(k) === 'param',
|
|
906
|
+
kIsVolatile: volatileLocals.has(k),
|
|
907
|
+
dupStep,
|
|
908
|
+
volatileWalks: walks.filter((p) => volatileLocals.has(p)),
|
|
909
|
+
coreHasExit: hasLoopExit(bodyCore),
|
|
910
|
+
badBases,
|
|
911
|
+
kMentions: countMentions(sfn.body, k),
|
|
912
|
+
unconfined: walks.filter((p) => countMentions(sfn.body, p) !== countMentions(shape, p)),
|
|
913
|
+
badStrides: walks.filter((p) => {
|
|
914
|
+
const es = elemSize(ptrVars.get(p));
|
|
915
|
+
return es === 0 || !derefWidths([loop], p).every((w) => w === es);
|
|
916
|
+
}),
|
|
917
|
+
leakyLeftovers: leftovers.filter((x) => walks.some((w) => stmtMentions(x, w))).length,
|
|
918
|
+
};
|
|
919
|
+
if (firstRejection(gates, ctx) !== null) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
// The caller's own admission. It runs before anything is minted, and so does every rule
|
|
923
|
+
// above: the rewrite below is the first statement in this function that mutates `locals` or
|
|
924
|
+
// `keptWalks`, so a decline has nothing to undo.
|
|
925
|
+
if (accept && !accept(leftovers)) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
// rewrite: each walk pointer is its OWN base (kept local, step dropped)
|
|
929
|
+
const iv = nextIv();
|
|
930
|
+
let body: Stmt[] | null = bodyCore;
|
|
931
|
+
for (const p of walks) {
|
|
932
|
+
body = body === null ? null : reindexStmts(body, { p, base: p }, iv);
|
|
933
|
+
}
|
|
934
|
+
if (body === null) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
declareIv(iv);
|
|
938
|
+
for (const p of walks) {
|
|
939
|
+
keptWalks?.add(p);
|
|
940
|
+
}
|
|
941
|
+
// Nothing mentions the counter past this point: its init, its decrement and the exit test are
|
|
942
|
+
// all gone, and the four-roles rule says those were every mention it had. Its declaration goes
|
|
943
|
+
// with them — a local declared, never written and never read reads as the deliberately
|
|
944
|
+
// uninitialized slot `uninit` exists to mark. A PARAM keeps its declaration, having one for a
|
|
945
|
+
// reason the body does not decide.
|
|
946
|
+
const kDecl = locals.findIndex((l) => l.name === k);
|
|
947
|
+
if (kDecl >= 0) {
|
|
948
|
+
locals.splice(kDecl, 1);
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
inductionInits,
|
|
952
|
+
leftovers,
|
|
953
|
+
counted: {
|
|
954
|
+
k: 'for',
|
|
955
|
+
init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
|
|
956
|
+
cond: { k: 'bin', op: '<', l: { k: 'var', name: iv }, r: trip },
|
|
957
|
+
inc: {
|
|
958
|
+
k: 'assign',
|
|
959
|
+
name: iv,
|
|
960
|
+
value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
|
|
961
|
+
},
|
|
962
|
+
body,
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** v2: `s` re-spelled as [C…, walk inits…, for], or null (decline). */
|
|
968
|
+
function respellGuardedCountdown(s: Stmt & { k: 'if' }): Stmt[] | null {
|
|
969
|
+
// which arm holds the loop, and does the guard skip it in the right sense?
|
|
970
|
+
const armWith = (arm: Stmt[]): { pre: Stmt[]; loop: Stmt & { k: 'dowhile' } } | null => {
|
|
971
|
+
const last = arm[arm.length - 1];
|
|
972
|
+
return last?.k === 'dowhile' ? { pre: arm.slice(0, -1), loop: last } : null;
|
|
973
|
+
};
|
|
974
|
+
const inElse = armWith(s.else);
|
|
975
|
+
const inThen = armWith(s.then);
|
|
976
|
+
const c = s.cond;
|
|
977
|
+
const guardSkips = (n: string, loopInElse: boolean): boolean => {
|
|
978
|
+
if (c.k !== 'bin') {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
const zl = c.l.k === 'const' && c.l.value === 0 && c.r.k === 'var' && c.r.name === n;
|
|
982
|
+
const zr = c.r.k === 'const' && c.r.value === 0 && c.l.k === 'var' && c.l.name === n;
|
|
983
|
+
// loop in else ⇒ cond true skips: 0 >= n / n <= 0. Loop in then ⇒ cond true enters: 0 < n / n > 0.
|
|
984
|
+
return loopInElse ? (zl && c.op === '>=') || (zr && c.op === '<=') : (zl && c.op === '<') || (zr && c.op === '>');
|
|
985
|
+
};
|
|
986
|
+
const pick = inElse ?? inThen;
|
|
987
|
+
const skipArm = inElse ? s.then : s.else;
|
|
988
|
+
if (!pick || (inElse && inThen)) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
const { pre, loop } = pick;
|
|
992
|
+
// the do-while exit: exactly `k != 0`
|
|
993
|
+
const lc = loop.cond;
|
|
994
|
+
if (lc.k !== 'bin' || lc.op !== '!=' || lc.l.k !== 'var' || lc.r.k !== 'const' || lc.r.value !== 0) {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
const k = lc.l.name;
|
|
998
|
+
// the counter's init `k = N`, N a var the guard tests
|
|
999
|
+
const kInit = pre.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === k);
|
|
1000
|
+
if (!kInit || kInit.value.k !== 'var') {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
const n = kInit.value.name;
|
|
1004
|
+
if (n === k || !guardSkips(n, pick === inElse)) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
if (sfn.body.some((x) => stmtAssigns(x, n))) {
|
|
1008
|
+
return null; // a moving bound
|
|
1009
|
+
}
|
|
1010
|
+
// the skip arm must be, in order, exactly `pre` minus the induction inits
|
|
1011
|
+
const skipArmMatches = (leftovers: Stmt[]): boolean =>
|
|
1012
|
+
skipArm.length === leftovers.length &&
|
|
1013
|
+
skipArm.every((x, i) => JSON.stringify(x) === JSON.stringify(leftovers[i]));
|
|
1014
|
+
const r = respellCountdown(loop, pre, k, kInit, { k: 'var', name: n }, () => [s], skipArmMatches);
|
|
1015
|
+
if (!r) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
return [...pre.filter((x) => x !== kInit), r.counted];
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** v4: the UNGUARDED constant-trip countdown (see the header). `out` holds the statements
|
|
1022
|
+
* already emitted before `dw`, the induction inits among them. Returns the list replacing
|
|
1023
|
+
* `out` (itself minus the counter's init) and the counted `for`, or null (decline). */
|
|
1024
|
+
function respellConstCountdown(out: Stmt[], dw: Stmt & { k: 'dowhile' }): { pre: Stmt[]; counted: Stmt } | null {
|
|
1025
|
+
// the do-while exit: exactly `k >= 0` / `0 <= k`
|
|
1026
|
+
const lc = dw.cond;
|
|
1027
|
+
if (lc.k !== 'bin') {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
const kv =
|
|
1031
|
+
lc.op === '>=' && lc.l.k === 'var' && lc.r.k === 'const' && lc.r.value === 0
|
|
1032
|
+
? lc.l.name
|
|
1033
|
+
: lc.op === '<=' && lc.r.k === 'var' && lc.l.k === 'const' && lc.l.value === 0
|
|
1034
|
+
? lc.r.name
|
|
1035
|
+
: null;
|
|
1036
|
+
if (kv === null) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
// `k >= 0` terminates only for a SIGNED counter — an unsigned one never goes below zero, and
|
|
1040
|
+
// the compiler that emitted this test proved it signed (`bge`). A declaration asmlift settled
|
|
1041
|
+
// on unsigned describes a different loop, so the trip count `C + 1` would be a fiction.
|
|
1042
|
+
const kT = declTypes(kv);
|
|
1043
|
+
if (kT?.kind !== 'int' || !kT.signed) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
// the counter's init `k = C`, C a NON-NEGATIVE constant: the loop runs while k counts C…0, so
|
|
1047
|
+
// the trip count is C + 1. That is a count only while BOTH ends are representable in the
|
|
1048
|
+
// counter's OWN type — below zero `k = -1` runs the body once rather than `C + 1 = 0` times,
|
|
1049
|
+
// and above its maximum `k = 200` in an `s8` is -56, which fails `k >= 0` at the first test
|
|
1050
|
+
// and runs the body once where `i < 201` runs it 201 times.
|
|
1051
|
+
const kMax = 2 ** (kT.width - 1) - 1;
|
|
1052
|
+
const kInit = out.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === kv);
|
|
1053
|
+
if (!kInit || kInit.value.k !== 'const' || kInit.value.value < 0 || kInit.value.value >= kMax) {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
const r = respellCountdown(dw, out, kv, kInit, { k: 'const', value: kInit.value.value + 1 }, (i) => i);
|
|
1057
|
+
if (!r) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return { pre: out.filter((x) => x !== kInit), counted: r.counted };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** The next free induction name: the lowest `i<N>` nothing in `sfn` already claims and no
|
|
1064
|
+
* earlier mint has COMMITTED. Pipeline naming is `a`/`v`/`t` prefixed, but a global is
|
|
1065
|
+
* referenced by bare name and future naming (DWARF) may import real source ones, so the
|
|
1066
|
+
* collision set is `takenNames`' — declarations, mentions, call targets and assignment
|
|
1067
|
+
* targets alike — not the declaration lists.
|
|
1068
|
+
*
|
|
1069
|
+
* PURE: it reads that snapshot and the locals `declareIv` committed, so a recognizer that
|
|
1070
|
+
* mints and then declines leaves the next mint returning the same name. Callers hold at most
|
|
1071
|
+
* one uncommitted name at a time — every mint site rewrites and commits before the walk
|
|
1072
|
+
* recursion can reach another loop. */
|
|
1073
|
+
function nextIv(): string {
|
|
1074
|
+
let n = 0;
|
|
1075
|
+
while (taken.has(`i${n}`) || locals.some((x) => x.name === `i${n}`)) {
|
|
1076
|
+
n++;
|
|
1077
|
+
}
|
|
1078
|
+
return `i${n}`;
|
|
443
1079
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
1080
|
+
/** Commit a minted name: the ONE place the induction local is declared, called only once the
|
|
1081
|
+
* rewrite has succeeded. */
|
|
1082
|
+
function declareIv(name: string): void {
|
|
1083
|
+
locals.push({ name, type: T.s(32) });
|
|
447
1084
|
}
|
|
448
1085
|
|
|
449
1086
|
const body = walkList(sfn.body);
|