@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,227 @@
|
|
|
1
|
+
// L3 respell variation: declare a recovered WORD field a POINTER (`void *field_4;` rather than
|
|
2
|
+
// `s32 field_4;`), and cast at each read.
|
|
3
|
+
//
|
|
4
|
+
// raise/structs.ts recovers a field's type from the ACCESS WIDTH alone — a 4-byte load is `s32`,
|
|
5
|
+
// because that is all a load says. On a 32-bit target `void *` fits the same evidence exactly:
|
|
6
|
+
// the two spellings load the same word with the same instruction, so the asm cannot referee them
|
|
7
|
+
// and the struct recovery's own byte-neutrality note applies to the field TYPE as much as to the
|
|
8
|
+
// `->field_N` vs `[idx]` question it was written about.
|
|
9
|
+
//
|
|
10
|
+
// IT IS NOT NEUTRAL TO THE COMPILER, which is the whole reason to spell it. A pointer and an `s32`
|
|
11
|
+
// are different alias sets, so under strict aliasing the loop optimizer may hoist a pointer field's
|
|
12
|
+
// load past an `s32`-typed store it must otherwise keep behind — and on synthetic:dmaptrsrc that
|
|
13
|
+
// hoist is the difference between a byte-exact match and a 35-point diff, once the accumulator is
|
|
14
|
+
// un-reduced back into the loop (that row's fan: `/vol-store/unreduce/ptr-field` 0,
|
|
15
|
+
// `/vol-store/unreduce` 35). Which side matches is per-field knowledge nothing in the asm
|
|
16
|
+
// carries, so both are enumerated and the differ referees.
|
|
17
|
+
//
|
|
18
|
+
// WHERE THE FLAG IS, because two readers have now looked for `-fstrict-aliasing` in the benchmark's
|
|
19
|
+
// agbcc line and not found it: it is not there, and it does not need to be. `-O2` turns it on
|
|
20
|
+
// (gcc 2.9 sets `flag_strict_aliasing` from `optimize >= 2`), which is why the CITATION is a
|
|
21
|
+
// behaviour rather than a flag. Verified in BOTH directions on this row's own shape, objects
|
|
22
|
+
// compared byte-for-byte:
|
|
23
|
+
// `void *` field, harness flags → the field load is HOISTED above the loop label
|
|
24
|
+
// `s32` field, harness flags → it STAYS in the loop body
|
|
25
|
+
// `void *` field, + -fno-strict-aliasing → byte-IDENTICAL to the `s32` build
|
|
26
|
+
// `s32` field, + -fstrict-aliasing → byte-IDENTICAL to itself (the flag was already on)
|
|
27
|
+
// So adding the flag proves nothing and REMOVING it proves everything, which is the direction a
|
|
28
|
+
// reader checking this note should take. The stores the load is hoisted past are the loop's own
|
|
29
|
+
// `*(volatile s32 *)0x040000D4/D8/DC` device writes — `volatile` restricts what may be done with
|
|
30
|
+
// THOSE accesses and does not merge their type into the pointer's alias set.
|
|
31
|
+
//
|
|
32
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION on a target whose pointers are 32 bits: the declaration
|
|
33
|
+
// changes, and every read is wrapped in a cast back to the field's own recovered integer type, so
|
|
34
|
+
// each use computes the same value it did. Nothing else moves. THE 32-BIT ASSUMPTION IS ASSERTED,
|
|
35
|
+
// NOT CHECKED — there is no pointer-width field on TargetDescription to check it against, and the
|
|
36
|
+
// assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
|
|
37
|
+
// a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
|
|
38
|
+
// width field is what that target's first row must add, and this note is where to start.
|
|
39
|
+
//
|
|
40
|
+
// IT FLIPS EVERY ADMITTED FIELD AT ONCE, and its own paragraph above says the knowledge is
|
|
41
|
+
// PER-FIELD — so the subset enumeration `l3/volatileptr.ts` does for exactly this reason
|
|
42
|
+
// (`volatileSubsetCandidates`, capped at three locals) is the shape this variation will eventually
|
|
43
|
+
// want. It is not built yet because nothing demands it: swept over 834 corpus trees, the variation
|
|
44
|
+
// fires on 42, and 34 of those have a single admitted field. The six 2-field trees and the two
|
|
45
|
+
// 4-field ones (`sa3:sa2__sub_8083504` flips Struct0.field_8/12 and Struct2.field_8/12 together)
|
|
46
|
+
// are where 1 of 3 and 1 of 15 non-empty subsets is reachable. A row that needs one of the missing
|
|
47
|
+
// subsets is what earns the enumeration — and it will cost candidates, which is why "it might
|
|
48
|
+
// help" is not enough.
|
|
49
|
+
//
|
|
50
|
+
// GATE (PTR_FIELD_GATES, read once per field): the field's recovered type must be a 32-bit
|
|
51
|
+
// integer; it must never be a store LVALUE (the write side would need a cast on the value, which
|
|
52
|
+
// is a second question); and it must never stand as the BASE of another access (a field the tree
|
|
53
|
+
// already dereferences is one the recovery typed from its own use, not from a width). Nothing
|
|
54
|
+
// qualifying ⇒ decline (null).
|
|
55
|
+
//
|
|
56
|
+
// SCOPE, because both of this table's blind spots look exactly like a gate refusing. First, THERE
|
|
57
|
+
// IS NO PAD RULE, deliberately: `plan` builds a ctx only for fields the tree ACCESSES, so "a field
|
|
58
|
+
// nothing reaches" cannot arise — and a rule for it reads as one, then answers for the 78 corpus
|
|
59
|
+
// WRITE-ONLY fields that `written` owns. Second, a field whose
|
|
60
|
+
// BASE TYPE does not resolve is dropped by `plan` before any gate reads it — 151 of the corpus's
|
|
61
|
+
// 820 field nodes, on 20 trees, all of them `[map]` configurations (kleod:CheckTileCollisionVertical,
|
|
62
|
+
// FreeAllDecompBuffers, TransformSingleEntityToScreen and ConfigureEntityBehavior among them).
|
|
63
|
+
// Declining there is right; being unable to say which of the two happened is the defect.
|
|
64
|
+
import { type IrType, T } from '../ir/types';
|
|
65
|
+
import {
|
|
66
|
+
type Expr,
|
|
67
|
+
type SFn,
|
|
68
|
+
type Stmt,
|
|
69
|
+
type StructType,
|
|
70
|
+
mapExprChildren,
|
|
71
|
+
mapStmtExprs,
|
|
72
|
+
stmtChildren,
|
|
73
|
+
walkExprs,
|
|
74
|
+
} from './ast';
|
|
75
|
+
import { type Gate, firstRejection } from './gates';
|
|
76
|
+
import { declaredTypes, exprCType } from './typing';
|
|
77
|
+
|
|
78
|
+
/** One recovered field as the gates read it — built only for fields the tree accesses. */
|
|
79
|
+
interface FieldCtx {
|
|
80
|
+
/** the recovered type is a 32-bit integer — the width a pointer also fits */
|
|
81
|
+
word: boolean;
|
|
82
|
+
written: boolean;
|
|
83
|
+
/** the field's value stands as the base of an `index` or another `field` */
|
|
84
|
+
dereferenced: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const PTR_FIELD_GATES: readonly Gate<FieldCtx>[] = [
|
|
88
|
+
{
|
|
89
|
+
id: 'not-word',
|
|
90
|
+
why: 'only a word-wide field fits a pointer as well as it fits an integer',
|
|
91
|
+
sound: true,
|
|
92
|
+
guardedBy: 'ptrfield.test.ts: a halfword field declines',
|
|
93
|
+
rejects: (c) => !c.word,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'written',
|
|
97
|
+
why: 'the write side needs a cast on the stored value, which is a separate question',
|
|
98
|
+
sound: true,
|
|
99
|
+
guardedBy: 'ptrfield.test.ts: a field the tree stores through declines',
|
|
100
|
+
rejects: (c) => c.written,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'dereferenced',
|
|
104
|
+
why: 'a field the tree already dereferences was typed from its use, not from a width',
|
|
105
|
+
sound: true,
|
|
106
|
+
guardedBy: 'ptrfield.test.ts: a field standing as an access base declines',
|
|
107
|
+
rejects: (c) => c.dereferenced,
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/** the struct a `field` node selects from, or null */
|
|
112
|
+
function structOf(base: Expr, vt: ReturnType<typeof declaredTypes>): Extract<IrType, { kind: 'struct' }> | null {
|
|
113
|
+
const t = exprCType(base, vt);
|
|
114
|
+
const st = t?.kind === 'ptr' ? t.to : t;
|
|
115
|
+
return st?.kind === 'struct' ? st : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `<struct>.<field>` as one key — two structs may both carry a `field_4`. */
|
|
119
|
+
const keyOf = (structName: string, field: string): string => `${structName}.${field}`;
|
|
120
|
+
|
|
121
|
+
/** The fields PTR_FIELD_GATES admits, keyed by struct and name, each with the integer type its
|
|
122
|
+
* reads cast back to. */
|
|
123
|
+
function plan(sfn: SFn): Map<string, IrType> {
|
|
124
|
+
const vt = declaredTypes(sfn);
|
|
125
|
+
const seen = new Map<string, FieldCtx & { type: IrType }>();
|
|
126
|
+
const note = (base: Expr, name: string, edit: (c: FieldCtx) => void): void => {
|
|
127
|
+
const st = structOf(base, vt);
|
|
128
|
+
const declared = st?.fields.find((f) => f.name === name)?.type;
|
|
129
|
+
if (st === null || declared === undefined) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const key = keyOf(st.name, name);
|
|
133
|
+
const cur = seen.get(key) ?? {
|
|
134
|
+
word: declared.kind === 'int' && declared.width === 32,
|
|
135
|
+
written: false,
|
|
136
|
+
dereferenced: false,
|
|
137
|
+
type: declared,
|
|
138
|
+
};
|
|
139
|
+
edit(cur);
|
|
140
|
+
seen.set(key, cur);
|
|
141
|
+
};
|
|
142
|
+
for (const e of walkExprs(sfn.body)) {
|
|
143
|
+
if (e.k === 'field') {
|
|
144
|
+
note(e.base, e.name, () => {});
|
|
145
|
+
}
|
|
146
|
+
// a field standing as an access base is one the recovery typed from its own use
|
|
147
|
+
const inner = e.k === 'index' ? e.base : e.k === 'field' ? e.base : null;
|
|
148
|
+
if (inner?.k === 'field') {
|
|
149
|
+
note(inner.base, inner.name, (c) => (c.dereferenced = true));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const s of stores(sfn.body)) {
|
|
153
|
+
if (s.lval.k === 'field') {
|
|
154
|
+
note(s.lval.base, s.lval.name, (c) => (c.written = true));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const out = new Map<string, IrType>();
|
|
158
|
+
for (const [key, ctx] of seen) {
|
|
159
|
+
if (firstRejection(PTR_FIELD_GATES, ctx) === null) {
|
|
160
|
+
out.set(key, ctx.type);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every `store` in the tree. ORDER-FREE by construction, which is what lets it use the shared
|
|
167
|
+
* `stmtChildren` (a switch's default sits at `defaultAt`, not last): its one caller only sets
|
|
168
|
+
* `written` on entries the preceding `walkExprs` pass already created — `note` builds an entry
|
|
169
|
+
* from the DECLARED field type, a function of the key alone, so this loop mints no key. */
|
|
170
|
+
function* stores(body: readonly Stmt[]): Generator<Extract<Stmt, { k: 'store' }>> {
|
|
171
|
+
for (const s of body) {
|
|
172
|
+
if (s.k === 'store') {
|
|
173
|
+
yield s;
|
|
174
|
+
}
|
|
175
|
+
yield* stores(stmtChildren(s));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The `/ptr-field` candidate, or null when no field qualifies. Read-only: returns a fresh SFn.
|
|
180
|
+
*
|
|
181
|
+
* The struct type appears TWICE in a tree — inline in each `(struct S *)` cast and again in
|
|
182
|
+
* `SFn.structs`, which is what a backend prints — so both are re-typed here. Letting them drift
|
|
183
|
+
* would print a declaration the expressions' own types contradict. */
|
|
184
|
+
export function pointerFields(sfn: SFn): SFn | null {
|
|
185
|
+
const admitted = plan(sfn);
|
|
186
|
+
if (admitted.size === 0) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const flipStruct = <S extends { name: string; fields: { name: string; type: IrType }[] }>(st: S): S => ({
|
|
190
|
+
...st,
|
|
191
|
+
fields: st.fields.map((f) => (admitted.has(keyOf(st.name, f.name)) ? { ...f, type: T.ptr(T.void()) } : f)),
|
|
192
|
+
});
|
|
193
|
+
const flip = (t: IrType): IrType => {
|
|
194
|
+
switch (t.kind) {
|
|
195
|
+
case 'struct':
|
|
196
|
+
return flipStruct(t);
|
|
197
|
+
case 'ptr':
|
|
198
|
+
return T.ptr(flip(t.to));
|
|
199
|
+
case 'array':
|
|
200
|
+
return T.array(flip(t.elem), t.count);
|
|
201
|
+
default:
|
|
202
|
+
return t;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const vt = declaredTypes(sfn);
|
|
206
|
+
const sub = (e: Expr): Expr => {
|
|
207
|
+
if (e.k === 'field') {
|
|
208
|
+
const st = structOf(e.base, vt);
|
|
209
|
+
const back = st === null ? undefined : admitted.get(keyOf(st.name, e.name));
|
|
210
|
+
const inner = mapExprChildren(e, sub);
|
|
211
|
+
return back === undefined ? inner : { k: 'cast', to: back, e: inner };
|
|
212
|
+
}
|
|
213
|
+
const m = mapExprChildren(e, sub);
|
|
214
|
+
return m.k === 'cast' ? { ...m, to: flip(m.to) } : m;
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
...sfn,
|
|
218
|
+
params: sfn.params.map((p) => ({ ...p, type: flip(p.type) })),
|
|
219
|
+
locals: sfn.locals.map((l) => ({ ...l, type: flip(l.type) })),
|
|
220
|
+
...(sfn.globals ? { globals: sfn.globals.map((g) => ({ ...g, type: flip(g.type) })) } : {}),
|
|
221
|
+
...(sfn.structs ? { structs: sfn.structs.map((s): StructType => flipStruct(s)) } : {}),
|
|
222
|
+
// `mapStmtExprs` already recurses into nested statement lists, so ONE call per top-level
|
|
223
|
+
// statement rewrites the whole subtree — a second walk over its children would apply `sub`
|
|
224
|
+
// twice and cast each read back to an integer twice over.
|
|
225
|
+
body: sfn.body.map((s) => mapStmtExprs(s, sub)),
|
|
226
|
+
};
|
|
227
|
+
}
|
package/src/l3/regspell.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// asmlift L3 — the REGISTER-COPY
|
|
1
|
+
// asmlift L3 — the REGISTER-COPY respell variation, differ-ranked like the three before it (the fourth,
|
|
2
2
|
// after signedness / branch sense / walk-vs-index).
|
|
3
3
|
//
|
|
4
4
|
// A compiler's register allocation leaves SOURCE-visible footprints the coalescing structurer
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// a bin operand materializes into its own fresh local first (`v = 128 << 9; … v / x …`) —
|
|
27
27
|
// the register the compiler staged the constant in.
|
|
28
28
|
// R3 — return assign-back: a non-var return expression lands in a fresh local first
|
|
29
|
-
// (`r = E; return r`). Emitted as a SEPARATE
|
|
29
|
+
// (`r = E; return r`). Emitted as a SEPARATE result (with/without R3) when R1/R2 fired:
|
|
30
30
|
// which tail the source spelled is itself ambiguous.
|
|
31
31
|
import { IrType, T } from '../ir/types';
|
|
32
32
|
import { Expr, SFn, Stmt, exprEquals, mapExprChildren } from './ast';
|
|
@@ -117,9 +117,106 @@ function isConstExpr(e: Expr): boolean {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/**
|
|
121
|
-
*
|
|
122
|
-
|
|
120
|
+
/** ONE R3 TAIL, NAMED BY WHAT IT DID rather than by where it landed in the list.
|
|
121
|
+
*
|
|
122
|
+
* The tail is the assign-back before the `return`, and there are two spellings of it: reuse R1's
|
|
123
|
+
* now-dead value var, or take a fresh one. R1 may not have fired, in which case there is no dead
|
|
124
|
+
* var and only the fresh spelling exists — so the tails are a 1- OR 2-element list and the SECOND
|
|
125
|
+
* spelling is not at a fixed index.
|
|
126
|
+
*
|
|
127
|
+
* This field exists because the caller names these, and a candidate's variations are the instrument
|
|
128
|
+
* every census in this repo reads (a `bench diff` FIELD). Index a `['/regcopy', '/regcopy-ret',
|
|
129
|
+
* '/regcopy-ret-fresh']` table by POSITION instead and an R1-less function publishes its fresh
|
|
130
|
+
* tail as `/regcopy-ret` — the dead-var-reuse name on the spelling that has no dead var to reuse
|
|
131
|
+
* — and a census over the token `/regcopy-ret-fresh` then censuses nothing wherever R1 declines,
|
|
132
|
+
* which is most of the corpus. */
|
|
133
|
+
export type RegcopyTail = 'none' | 'reuse' | 'fresh';
|
|
134
|
+
|
|
135
|
+
export interface RegcopySpelling {
|
|
136
|
+
/** which R3 tail this result carries — `none` is the un-tailed base */
|
|
137
|
+
tail: RegcopyTail;
|
|
138
|
+
sfn: SFn;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface Diamond {
|
|
142
|
+
v: string;
|
|
143
|
+
E: Expr;
|
|
144
|
+
updArm: 'then' | 'else';
|
|
145
|
+
upd: Expr;
|
|
146
|
+
cond: Expr;
|
|
147
|
+
}
|
|
148
|
+
/** `if (cmp) { v = E } else { v = f(E) }` (or arms swapped), f = bin(E, const-ish). */
|
|
149
|
+
function matchDiamond(s: Extract<Stmt, { k: 'if' }>): Diamond | null {
|
|
150
|
+
if (s.then.length !== 1 || s.else.length !== 1) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const a = s.then[0];
|
|
154
|
+
const b = s.else[0];
|
|
155
|
+
if (a.k !== 'assign' || b.k !== 'assign' || a.name !== b.name) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const isUpdOf = (upd: Expr, base: Expr): boolean =>
|
|
159
|
+
upd.k === 'bin' && isPure(upd.r) && exprEq(upd.l, base) && isConstExpr(upd.r);
|
|
160
|
+
if (isUpdOf(b.value, a.value)) {
|
|
161
|
+
return { v: a.name, E: a.value, updArm: 'else', upd: b.value, cond: s.cond };
|
|
162
|
+
}
|
|
163
|
+
if (isUpdOf(a.value, b.value)) {
|
|
164
|
+
return { v: a.name, E: b.value, updArm: 'then', upd: a.value, cond: s.cond };
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/** Does the cond's NON-E operand mention `v`? (The E side becomes the copy; the other side
|
|
169
|
+
* must be v-free or the hoisted assignment changes what it compares against.) */
|
|
170
|
+
function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
|
|
171
|
+
if (cond.k !== 'bin') {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const other = exprEq(cond.l, E) ? cond.r : exprEq(cond.r, E) ? cond.l : null;
|
|
175
|
+
const mentions = (e: Expr): boolean => {
|
|
176
|
+
if (e.k === 'var') {
|
|
177
|
+
return e.name === v;
|
|
178
|
+
}
|
|
179
|
+
let hit = false;
|
|
180
|
+
mapExprChildren(e, (c) => {
|
|
181
|
+
hit = hit || mentions(c);
|
|
182
|
+
return c;
|
|
183
|
+
});
|
|
184
|
+
return hit;
|
|
185
|
+
};
|
|
186
|
+
return other ? mentions(other) : false;
|
|
187
|
+
}
|
|
188
|
+
/** cond compares E against a pure operand → same comparison reading the named var. */
|
|
189
|
+
function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
|
|
190
|
+
if (cond.k !== 'bin' || !(cond.op in FLIP)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (exprEq(cond.l, E) && isPure(cond.r)) {
|
|
194
|
+
return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
|
|
195
|
+
}
|
|
196
|
+
if (exprEq(cond.r, E) && isPure(cond.l)) {
|
|
197
|
+
return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function flipCmp(cond: Expr): Expr | null {
|
|
202
|
+
if (cond.k !== 'bin') {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const op = FLIP[cond.op];
|
|
206
|
+
return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
|
|
207
|
+
}
|
|
208
|
+
/** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
|
|
209
|
+
function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
|
|
210
|
+
if (exprEq(e, E)) {
|
|
211
|
+
return { k: 'var', name };
|
|
212
|
+
}
|
|
213
|
+
return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Apply the register-copy re-spelling. Returns 0–3 results — the base, plus the R3 tail in each
|
|
217
|
+
* spelling that exists (reuse needs R1 to have fired, fresh always does) — and an EMPTY list when
|
|
218
|
+
* nothing fired. Pure — never mutates the input. */
|
|
219
|
+
export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
123
220
|
const locals = [...sfn.locals];
|
|
124
221
|
const taken = new Set([...sfn.params, ...sfn.locals].map((x) => x.name));
|
|
125
222
|
let fresh = 0;
|
|
@@ -150,7 +247,7 @@ export function registerishSpellings(sfn: SFn): SFn[] {
|
|
|
150
247
|
if (m) {
|
|
151
248
|
const { v, E, updArm, upd, cond } = m;
|
|
152
249
|
// GUARDS BEFORE ALLOCATION (a declined shape must leave no residue — the leaked
|
|
153
|
-
// dead `w` even perturbed the live-name count this
|
|
250
|
+
// dead `w` even perturbed the live-name count this variation exists to reproduce):
|
|
154
251
|
// • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
|
|
155
252
|
// `if (a < v)` would compare against the POST-assignment v — reproduced);
|
|
156
253
|
// • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
|
|
@@ -188,81 +285,6 @@ export function registerishSpellings(sfn: SFn): SFn[] {
|
|
|
188
285
|
return out;
|
|
189
286
|
};
|
|
190
287
|
|
|
191
|
-
interface Diamond {
|
|
192
|
-
v: string;
|
|
193
|
-
E: Expr;
|
|
194
|
-
updArm: 'then' | 'else';
|
|
195
|
-
upd: Expr;
|
|
196
|
-
cond: Expr;
|
|
197
|
-
}
|
|
198
|
-
/** `if (cmp) { v = E } else { v = f(E) }` (or arms swapped), f = bin(E, const-ish). */
|
|
199
|
-
function matchDiamond(s: Extract<Stmt, { k: 'if' }>): Diamond | null {
|
|
200
|
-
if (s.then.length !== 1 || s.else.length !== 1) {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
const a = s.then[0];
|
|
204
|
-
const b = s.else[0];
|
|
205
|
-
if (a.k !== 'assign' || b.k !== 'assign' || a.name !== b.name) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
const isUpdOf = (upd: Expr, base: Expr): boolean =>
|
|
209
|
-
upd.k === 'bin' && isPure(upd.r) && exprEq(upd.l, base) && isConstExpr(upd.r);
|
|
210
|
-
if (isUpdOf(b.value, a.value)) {
|
|
211
|
-
return { v: a.name, E: a.value, updArm: 'else', upd: b.value, cond: s.cond };
|
|
212
|
-
}
|
|
213
|
-
if (isUpdOf(a.value, b.value)) {
|
|
214
|
-
return { v: a.name, E: b.value, updArm: 'then', upd: a.value, cond: s.cond };
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
/** Does the cond's NON-E operand mention `v`? (The E side becomes the copy; the other side
|
|
219
|
-
* must be v-free or the hoisted assignment changes what it compares against.) */
|
|
220
|
-
function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
|
|
221
|
-
if (cond.k !== 'bin') {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
const other = exprEq(cond.l, E) ? cond.r : exprEq(cond.r, E) ? cond.l : null;
|
|
225
|
-
const mentions = (e: Expr): boolean => {
|
|
226
|
-
if (e.k === 'var') {
|
|
227
|
-
return e.name === v;
|
|
228
|
-
}
|
|
229
|
-
let hit = false;
|
|
230
|
-
mapExprChildren(e, (c) => {
|
|
231
|
-
hit = hit || mentions(c);
|
|
232
|
-
return c;
|
|
233
|
-
});
|
|
234
|
-
return hit;
|
|
235
|
-
};
|
|
236
|
-
return other ? mentions(other) : false;
|
|
237
|
-
}
|
|
238
|
-
/** cond compares E against a pure operand → same comparison reading the named var. */
|
|
239
|
-
function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
|
|
240
|
-
if (cond.k !== 'bin' || !(cond.op in FLIP)) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
if (exprEq(cond.l, E) && isPure(cond.r)) {
|
|
244
|
-
return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
|
|
245
|
-
}
|
|
246
|
-
if (exprEq(cond.r, E) && isPure(cond.l)) {
|
|
247
|
-
return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
|
|
248
|
-
}
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
function flipCmp(cond: Expr): Expr | null {
|
|
252
|
-
if (cond.k !== 'bin') {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
const op = FLIP[cond.op];
|
|
256
|
-
return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
|
|
257
|
-
}
|
|
258
|
-
/** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
|
|
259
|
-
function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
|
|
260
|
-
if (exprEq(e, E)) {
|
|
261
|
-
return { k: 'var', name };
|
|
262
|
-
}
|
|
263
|
-
return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
288
|
// R2: stage const-expressions used as bin operands into fresh locals, at statement level.
|
|
267
289
|
const r2Stmt = (s: Stmt): Stmt[] => {
|
|
268
290
|
const staged: Stmt[] = [];
|
|
@@ -305,27 +327,30 @@ export function registerishSpellings(sfn: SFn): SFn[] {
|
|
|
305
327
|
}
|
|
306
328
|
const base: SFn = { ...sfn, locals: [...locals], body: afterR2 };
|
|
307
329
|
|
|
308
|
-
// R3
|
|
330
|
+
// R3 results: the tail assign-back — a non-var return lands in a local first. WHICH local is
|
|
309
331
|
// itself allocator-ambiguous (gcc 2.9 wanted R1's dead value var — live-name-count sensitive;
|
|
310
332
|
// another allocator may want the fresh one), so BOTH tails are emitted as candidates rather
|
|
311
333
|
// than asserting one compiler's preference; the source dedupe collapses them when identical.
|
|
312
|
-
const tails:
|
|
334
|
+
const tails: RegcopySpelling[] = [];
|
|
313
335
|
const last = base.body[base.body.length - 1];
|
|
314
|
-
if (last?.k === 'return' && last.value && last.value.k !== 'var') {
|
|
336
|
+
if (last?.k === 'return' && last.value !== undefined && last.value.k !== 'var') {
|
|
337
|
+
// bound here rather than re-read inside `mk`: a PROPERTY's narrowing does not survive into a
|
|
338
|
+
// closure, and re-asserting it is what the three casts this replaces were doing
|
|
339
|
+
const retVal: Expr = last.value;
|
|
315
340
|
const mk = (name: string): SFn => ({
|
|
316
341
|
...base,
|
|
317
342
|
locals: [...locals],
|
|
318
343
|
body: [
|
|
319
344
|
...base.body.slice(0, -1),
|
|
320
|
-
{ k: 'assign', name, value:
|
|
321
|
-
{ k: 'return', value: { k: 'var', name } }
|
|
345
|
+
{ k: 'assign', name, value: retVal },
|
|
346
|
+
{ k: 'return', value: { k: 'var', name } },
|
|
322
347
|
],
|
|
323
348
|
});
|
|
324
349
|
if (deadValueVar) {
|
|
325
|
-
tails.push(mk(deadValueVar));
|
|
350
|
+
tails.push({ tail: 'reuse', sfn: mk(deadValueVar) });
|
|
326
351
|
}
|
|
327
|
-
tails.push(mk(freshVar(T.s(32))));
|
|
352
|
+
tails.push({ tail: 'fresh', sfn: mk(freshVar(T.s(32))) });
|
|
328
353
|
}
|
|
329
354
|
|
|
330
|
-
return [base, ...tails];
|
|
355
|
+
return [{ tail: 'none', sfn: base }, ...tails];
|
|
331
356
|
}
|