@asmlift/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// L3 poll-shape re-spelling levers: `pollGuards` regrows an empty bottom-tested loop's guard;
|
|
2
|
+
// `pollReads` folds a materialized poll's re-read back into its while condition. Each carries
|
|
3
|
+
// its own trace argument below.
|
|
4
|
+
//
|
|
5
|
+
// do { } while (dma[2] & 0x80000000); → if (dma[2] & 0x80000000) { do { } while (…); }
|
|
6
|
+
//
|
|
7
|
+
// For an empty body the two forms compile to the SAME instructions — gcc collapses the guard
|
|
8
|
+
// into the bottom test late (jump optimization), AFTER flow has counted the guard's reads — so
|
|
9
|
+
// the choice leaves no instruction trace, only a register-allocation ripple: the extra
|
|
10
|
+
// source-level read raises the condition operands' ref counts, which re-orders the allocator's
|
|
11
|
+
// priorities for the WHOLE function (the busy-wait's base landing in a low reg vs `ip`). Which
|
|
12
|
+
// form the source spelled is unrecoverable from the bytes; both are emitted and the differ
|
|
13
|
+
// referees.
|
|
14
|
+
//
|
|
15
|
+
// SCOPE (decline over approximate): only a `dowhile` with an EMPTY body regrows a guard —
|
|
16
|
+
// there the two forms have IDENTICAL evaluation traces (each evaluates the condition until its
|
|
17
|
+
// first falsy result; the regrown guard IS the first bottom-test, not an extra one), so
|
|
18
|
+
// volatile reads, calls, any effect in the condition all count the same. A NON-empty body is
|
|
19
|
+
// where the forms genuinely differ (the body runs at least once vs at least zero times), which
|
|
20
|
+
// is why it never wraps. Declines (null) when no empty do-while exists.
|
|
21
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
22
|
+
import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs } from './ast';
|
|
23
|
+
|
|
24
|
+
export function pollGuards(sfn: SFn): SFn | null {
|
|
25
|
+
let changed = false;
|
|
26
|
+
const rewrite = (s: Stmt): Stmt => {
|
|
27
|
+
if (s.k === 'dowhile' && s.body.length === 0) {
|
|
28
|
+
changed = true;
|
|
29
|
+
return { k: 'if', cond: s.cond, then: [s], else: [] };
|
|
30
|
+
}
|
|
31
|
+
return mapStmtLists(s, (list) => list.map(rewrite));
|
|
32
|
+
};
|
|
33
|
+
const body = sfn.body.map(rewrite);
|
|
34
|
+
return changed ? { ...sfn, body } : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// L3 re-spelling lever: a materialized POLL re-reads in its own condition.
|
|
38
|
+
//
|
|
39
|
+
// v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; } → while ((dma[2] & BUSY) != 0) {}
|
|
40
|
+
//
|
|
41
|
+
// The structurer materializes a loop-carried load into a named temp with a pre-loop read and a
|
|
42
|
+
// per-iteration re-read; the source may have spelled the read INSIDE the condition of an
|
|
43
|
+
// empty-bodied `while`. The two forms have IDENTICAL evaluation traces — the old form reads once
|
|
44
|
+
// before plus once per iteration, the new form reads once per condition evaluation, and both
|
|
45
|
+
// count 1 + iterations — so volatile reads COUNT the same; the condition gates below (call-free,
|
|
46
|
+
// no volatile-rooted or raw derefs) are what make the ORDER identical too: with no other
|
|
47
|
+
// observable effect in the condition, there is nothing for the embedded read to reorder against.
|
|
48
|
+
// What differs is bytes: the pre-read + temp spelling materializes an extra register and
|
|
49
|
+
// instruction the in-condition spelling does not.
|
|
50
|
+
//
|
|
51
|
+
// SCOPE (decline over approximate): each admission is one named predicate below and carries its own
|
|
52
|
+
// refusal's reason; the temp must additionally be the function's OWN non-volatile LOCAL, because a
|
|
53
|
+
// bare global's assigns are stores other code observes and its declaration cannot be dropped.
|
|
54
|
+
// Declines (null) when no poll matches.
|
|
55
|
+
const countVar = (e: Expr, n: string): number =>
|
|
56
|
+
(e.k === 'var' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countVar(c, n), 0);
|
|
57
|
+
|
|
58
|
+
const countAddr = (e: Expr, n: string): number =>
|
|
59
|
+
(e.k === 'addr' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countAddr(c, n), 0);
|
|
60
|
+
|
|
61
|
+
/** call- and marker-free: no effect the fold could move or duplicate. */
|
|
62
|
+
const pure = (e: Expr): boolean => e.k !== 'call' && e.k !== 'marker' && exprChildren(e).every(pure);
|
|
63
|
+
|
|
64
|
+
/** The var a deref's base stands on, through casts only — null for anything else (a raw address, an
|
|
65
|
+
* arithmetic base), which is exactly the set `condDerefsPlain` refuses. */
|
|
66
|
+
const rootVar = (e: Expr): string | null => (e.k === 'var' ? e.name : e.k === 'cast' ? rootVar(e.e) : null);
|
|
67
|
+
|
|
68
|
+
/** ORDER-safety for the condition's other reads: a deref there must be rooted at a var declared
|
|
69
|
+
* non-volatile — a volatile-rooted or raw-address deref is (or may be) an OBSERVABLE read the
|
|
70
|
+
* fold would unsequence against X inside one expression, where the original sequenced them. */
|
|
71
|
+
const condDerefsPlain = (e: Expr, volatileLocals: ReadonlySet<string>): boolean => {
|
|
72
|
+
if (e.k === 'index' || e.k === 'field') {
|
|
73
|
+
const rv = rootVar(e.base);
|
|
74
|
+
if (rv === null || volatileLocals.has(rv)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return exprChildren(e).every((c) => condDerefsPlain(c, volatileLocals));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Every mention of `n` in a statement list — reads, `&n`, and assign targets, at any depth. */
|
|
82
|
+
const occurs = (list: Stmt[], n: string): number =>
|
|
83
|
+
list.reduce(
|
|
84
|
+
(a, st) =>
|
|
85
|
+
a +
|
|
86
|
+
stmtExprs(st).reduce((x, e) => x + countVar(e, n) + countAddr(e, n), 0) +
|
|
87
|
+
(st.k === 'assign' && st.name === n ? 1 : 0) +
|
|
88
|
+
occurs(stmtChildren(st), n),
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/** The loop body must be EXACTLY the one re-read: a single assign of the same variable to the same
|
|
93
|
+
* expression. Any other statement there is one the fold would delete along with the loop's body. */
|
|
94
|
+
const bodyIsTheSoleReread = (w: Extract<Stmt, { k: 'while' }>, a: Extract<Stmt, { k: 'assign' }>): boolean =>
|
|
95
|
+
w.body.length === 1 && w.body[0].k === 'assign' && w.body[0].name === a.name && exprEquals(w.body[0].value, a.value);
|
|
96
|
+
|
|
97
|
+
/** The condition must read the variable EXACTLY once, as a bare var: a second read would double X's
|
|
98
|
+
* per-iteration evaluation, and an `&v` is not a read at all and cannot be substituted. */
|
|
99
|
+
const condReadsVarOnce = (cond: Expr, n: string): boolean => countVar(cond, n) === 1 && countAddr(cond, n) === 0;
|
|
100
|
+
|
|
101
|
+
/** X must not mention the variable it is assigned to — the folded form evaluates X in a condition
|
|
102
|
+
* where that variable no longer exists. */
|
|
103
|
+
const valueIsSelfFree = (value: Expr, n: string): boolean => countVar(value, n) === 0 && countAddr(value, n) === 0;
|
|
104
|
+
|
|
105
|
+
/** The condition carries no effect and no observable read of its own, which is what makes the
|
|
106
|
+
* embedded X unsequenceable against anything: call/marker-free, every deref `condDerefsPlain`. */
|
|
107
|
+
const condFoldable = (cond: Expr, volatileLocals: ReadonlySet<string>): boolean =>
|
|
108
|
+
pure(cond) && condDerefsPlain(cond, volatileLocals);
|
|
109
|
+
|
|
110
|
+
/** The pattern owns EVERY occurrence of the variable: both assign targets and the one condition
|
|
111
|
+
* read, three in all, counted over the whole function and counting `&v`. Its declaration is dropped
|
|
112
|
+
* with the temp, so anything else mentioning it would be left naming a variable that is gone. */
|
|
113
|
+
const ownsEveryOccurrence = (body: Stmt[], n: string): boolean => occurs(body, n) === 3;
|
|
114
|
+
|
|
115
|
+
export function pollReads(sfn: SFn): SFn | null {
|
|
116
|
+
const ownPlain = new Set(
|
|
117
|
+
sfn.locals.filter((l) => l.volatile !== true && l.pointeeVolatile !== true).map((l) => l.name),
|
|
118
|
+
);
|
|
119
|
+
const volatileLocals = new Set(
|
|
120
|
+
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
121
|
+
);
|
|
122
|
+
const subst = (e: Expr, n: string, x: Expr): Expr =>
|
|
123
|
+
e.k === 'var' && e.name === n ? x : mapExprChildren(e, (c) => subst(c, n, x));
|
|
124
|
+
const dropped = new Set<string>();
|
|
125
|
+
const rewriteList = (list: Stmt[]): Stmt[] => {
|
|
126
|
+
const out: Stmt[] = [];
|
|
127
|
+
for (let i = 0; i < list.length; i++) {
|
|
128
|
+
const a = list[i];
|
|
129
|
+
const w = list[i + 1];
|
|
130
|
+
if (
|
|
131
|
+
a.k === 'assign' &&
|
|
132
|
+
ownPlain.has(a.name) &&
|
|
133
|
+
w !== undefined &&
|
|
134
|
+
w.k === 'while' &&
|
|
135
|
+
bodyIsTheSoleReread(w, a) &&
|
|
136
|
+
condReadsVarOnce(w.cond, a.name) &&
|
|
137
|
+
valueIsSelfFree(a.value, a.name) &&
|
|
138
|
+
pure(a.value) &&
|
|
139
|
+
condFoldable(w.cond, volatileLocals) &&
|
|
140
|
+
ownsEveryOccurrence(sfn.body, a.name)
|
|
141
|
+
) {
|
|
142
|
+
out.push({ k: 'while', cond: subst(w.cond, a.name, a.value), body: [] });
|
|
143
|
+
dropped.add(a.name);
|
|
144
|
+
i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
out.push(recurse(a));
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
};
|
|
151
|
+
const recurse = (s0: Stmt): Stmt => mapStmtLists(s0, rewriteList);
|
|
152
|
+
const body = rewriteList(sfn.body);
|
|
153
|
+
return dropped.size > 0 ? { ...sfn, body, locals: sfn.locals.filter((l) => !dropped.has(l.name)) } : null;
|
|
154
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// L3 re-spelling lever: 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 lever 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 lever will eventually
|
|
43
|
+
// want. It is not built yet because nothing demands it: swept over 834 corpus trees, the lever
|
|
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
|
@@ -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 labels these, and a label is the instrument every census
|
|
128
|
+
* in this repo reads (it is 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 variant 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 variants — 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;
|
|
@@ -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[] = [];
|
|
@@ -309,23 +331,26 @@ export function registerishSpellings(sfn: SFn): SFn[] {
|
|
|
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
|
}
|