@asmlift/core 0.2.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +154 -5
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +37 -5
- package/src/declare.ts +251 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +420 -32
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +126 -6
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +335 -0
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +20 -6
- package/src/proto.ts +55 -0
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +370 -79
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +852 -67
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +541 -0
- package/src/target.ts +4 -2
- package/src/trace.ts +17 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { typeToString } from '../ir/types';
|
|
2
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
3
|
+
import { exprChildren, mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
4
|
+
|
|
5
|
+
function namesIn(e: Expr, out: Set<string>): void {
|
|
6
|
+
// `addr` names a GLOBAL, never a local — collected anyway. A name reaching BOTH forms would
|
|
7
|
+
// otherwise get a span that ignores its `addr` mentions, and a SHORT span is a clobber while a
|
|
8
|
+
// long one is only a missed merge. `structure.ts` keeps locals to /^[vt]\d+$/ and excludes global
|
|
9
|
+
// names, so this cannot fire today; collecting is the direction that stays safe if that changes.
|
|
10
|
+
if (e.k === 'var' || e.k === 'addr') out.add(e.name);
|
|
11
|
+
for (const c of exprChildren(e)) namesIn(c, out);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Does `e` mention `n` anywhere? */
|
|
15
|
+
function mentions(e: Expr, n: string): boolean {
|
|
16
|
+
const seen = new Set<string>();
|
|
17
|
+
namesIn(e, seen);
|
|
18
|
+
return seen.has(n);
|
|
19
|
+
}
|
|
20
|
+
interface Span {
|
|
21
|
+
first: number;
|
|
22
|
+
last: number;
|
|
23
|
+
inLoop: boolean;
|
|
24
|
+
constFed: boolean;
|
|
25
|
+
/** the local's FIRST mention is a write, not a read */
|
|
26
|
+
firstIsWrite: boolean;
|
|
27
|
+
}
|
|
28
|
+
function spans(body: Stmt[]): Map<string, Span> {
|
|
29
|
+
const out = new Map<string, Span>();
|
|
30
|
+
let at = 0;
|
|
31
|
+
const walk = (list: Stmt[], inLoop: boolean): void => {
|
|
32
|
+
for (const s of list) {
|
|
33
|
+
at++;
|
|
34
|
+
const here = new Set<string>();
|
|
35
|
+
if (s.k === 'assign') here.add(s.name);
|
|
36
|
+
for (const e of stmtExprs(s)) namesIn(e, here);
|
|
37
|
+
for (const n of here) {
|
|
38
|
+
const sp = out.get(n) ?? {
|
|
39
|
+
first: at,
|
|
40
|
+
last: at,
|
|
41
|
+
inLoop,
|
|
42
|
+
constFed: true,
|
|
43
|
+
// an assign that ALSO READS the name (`b = g(b)`) is not a pure write; treating it as one
|
|
44
|
+
// let `g` receive the absorbed value
|
|
45
|
+
firstIsWrite: s.k === 'assign' && s.name === n && !stmtExprs(s).some((e) => mentions(e, n)),
|
|
46
|
+
};
|
|
47
|
+
sp.last = at;
|
|
48
|
+
sp.inLoop ||= inLoop;
|
|
49
|
+
if (s.k === 'assign' && s.name === n && s.value.k !== 'const') sp.constFed = false;
|
|
50
|
+
out.set(n, sp);
|
|
51
|
+
}
|
|
52
|
+
walk(stmtChildren(s), inLoop || s.k === 'while' || s.k === 'dowhile' || s.k === 'for');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
walk(body, false);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function rename(body: Stmt[], from: string, to: string): Stmt[] {
|
|
59
|
+
const inExpr = (e: Expr): Expr =>
|
|
60
|
+
e.k === 'var' && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
|
|
61
|
+
const inStmt = (s: Stmt): Stmt => {
|
|
62
|
+
const r = { ...s } as Record<string, unknown>;
|
|
63
|
+
if (s.k === 'assign' && s.name === from) r.name = to;
|
|
64
|
+
for (const key of ['value', 'lval', 'cond', 'scrutinee'] as const) {
|
|
65
|
+
const v = (s as Record<string, unknown>)[key];
|
|
66
|
+
if (v !== undefined) r[key] = inExpr(v as Expr);
|
|
67
|
+
}
|
|
68
|
+
for (const key of ['then', 'else', 'body', 'default'] as const) {
|
|
69
|
+
const v = (s as Record<string, unknown>)[key];
|
|
70
|
+
if (Array.isArray(v)) r[key] = (v as Stmt[]).map(inStmt);
|
|
71
|
+
}
|
|
72
|
+
if (s.k === 'for') {
|
|
73
|
+
r.init = inStmt(s.init);
|
|
74
|
+
r.inc = inStmt(s.inc);
|
|
75
|
+
}
|
|
76
|
+
if (s.k === 'switch') r.cases = s.cases.map((c) => ({ ...c, body: c.body.map(inStmt) }));
|
|
77
|
+
return r as Stmt;
|
|
78
|
+
};
|
|
79
|
+
return body.map(inStmt);
|
|
80
|
+
}
|
|
81
|
+
/** Every legal single merge, each as its own tree — NOT one committed choice.
|
|
82
|
+
*
|
|
83
|
+
* Which pair a register allocator coalesced is not derivable from the L3 tree, and first-fit gets
|
|
84
|
+
* it wrong: on kleod:UpdateHUDCounterDisplay the two legal merges score 18 and 40 against a
|
|
85
|
+
* no-merge baseline of 21, and declaration order picks the 40. `rank.ts` already has the idiom for
|
|
86
|
+
* exactly this — `/regcopy`'s "the tail choice is allocator-ambiguous, so both are ranked" — so
|
|
87
|
+
* every candidate is emitted and the differ referees.
|
|
88
|
+
*
|
|
89
|
+
* GATES:
|
|
90
|
+
* - a local mentioned inside a loop BODY is excluded. SOUND-critical: it is what makes preorder
|
|
91
|
+
* statement order a sufficient approximation of liveness. Preorder is a topological order of the
|
|
92
|
+
* CFG except where a later-indexed statement can run before an earlier one, and the positions
|
|
93
|
+
* that do that — a `for`'s `init`/`inc`, and everything in any loop body — are inside a loop, so
|
|
94
|
+
* the gate covers them. A loop's own CONDITION is NOT covered: it is visited at the loop
|
|
95
|
+
* statement's own index with the ENCLOSING loop flag. That is safe only because a condition
|
|
96
|
+
* cannot WRITE, so it can extend a read range but never reorder a definition — an earlier
|
|
97
|
+
* version of this comment claimed the gate covered conditions too, which it does not.
|
|
98
|
+
* Differential fuzzing supports this: removing the gate produces clobbers immediately, leaving
|
|
99
|
+
* it on produces none. No such harness is committed, so nothing here re-checks it.
|
|
100
|
+
* - both must be CONSTANT-fed. A codegen heuristic, not soundness — removing it stayed
|
|
101
|
+
* clobber-free under the same (uncommitted) fuzz and simply scored worse, because a load-fed
|
|
102
|
+
* local is one the compiler had a reason to keep where it was. It is also what currently BOUNDS
|
|
103
|
+
* candidate growth: merges are `L(L-1)/2` in the local count, each a distinct source and so a
|
|
104
|
+
* distinct compile, and nothing else caps that. Corpus-wide today: 2 rows, 13 kept sources.
|
|
105
|
+
* - the survivor's first mention must be an ASSIGN THAT DOES NOT ALSO READ IT. `b = g(b)` is a
|
|
106
|
+
* write and a read in one statement; counting it as a pure write let `g` receive the absorbed
|
|
107
|
+
* value. These two gates are NOT independent: `constFed` also rejects a self-reading assign
|
|
108
|
+
* (its value is not a literal), so it masks this one. No committed test isolates it — this is
|
|
109
|
+
* defence-in-depth for the day `constFed` is relaxed, which the note above makes plausible.
|
|
110
|
+
*
|
|
111
|
+
* ACCEPTED, NOT FIXED: a survivor assigned only on SOME paths still absorbs the other's value on
|
|
112
|
+
* the paths that skip it. The original read an uninitialized local there, so both spellings are
|
|
113
|
+
* ill-defined rather than one being wrong — but this is a real difference and the differ, not this
|
|
114
|
+
* gate, is what keeps it from faking a match. */
|
|
115
|
+
export function coalesceCandidates(sfn: SFn): { merged: string; sfn: SFn }[] {
|
|
116
|
+
if (sfn.locals.length < 2) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const params = new Set(sfn.params.map((p) => p.name));
|
|
120
|
+
const typeOf = new Map(sfn.locals.map((l) => [l.name, typeToString(l.type)]));
|
|
121
|
+
const sp = spans(sfn.body);
|
|
122
|
+
const out: { merged: string; sfn: SFn }[] = [];
|
|
123
|
+
for (const a of sfn.locals.map((l) => l.name)) {
|
|
124
|
+
for (const b of sfn.locals.map((l) => l.name)) {
|
|
125
|
+
const x = sp.get(a);
|
|
126
|
+
const y = sp.get(b);
|
|
127
|
+
if (a === b || !x || !y || params.has(a) || params.has(b)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (typeOf.get(a) !== typeOf.get(b) || x.inLoop || y.inLoop || !x.constFed || !y.constFed) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (x.last >= y.first || !y.firstIsWrite) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Labelled by the PAIR, not by an index into enumeration order: an index silently re-points
|
|
137
|
+
// at a different merge if `sfn.locals` ordering ever changes, leaving a recorded provenance
|
|
138
|
+
// that is wrong but plausible.
|
|
139
|
+
out.push({
|
|
140
|
+
merged: `${a}-${b}`,
|
|
141
|
+
sfn: { ...sfn, body: rename(sfn.body, a, b), locals: sfn.locals.filter((l) => l.name !== a) },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
package/src/l3/dce.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
|
|
23
23
|
// keep is defense-in-depth for any future caller that skips that check.
|
|
24
24
|
import type { Expr, SFn, Stmt } from './ast';
|
|
25
|
-
import { exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
25
|
+
import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
26
26
|
|
|
27
27
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
28
28
|
* `addr` node names a global, not a local, so it is not a local read. */
|
|
@@ -70,27 +70,6 @@ function allReadsInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** Negate a condition, flipping a relational operator directly (`!= → ==`, `< → >=`, …) so an
|
|
74
|
-
* empty-then flip reads cleanly; anything else wraps in `!( … )`. Both forms are semantically
|
|
75
|
-
* exact over C's total integer order. */
|
|
76
|
-
function negate(cond: Expr): Expr {
|
|
77
|
-
if (cond.k === 'bin') {
|
|
78
|
-
const table: Record<string, '==' | '!=' | '<' | '<=' | '>' | '>='> = {
|
|
79
|
-
'==': '!=',
|
|
80
|
-
'!=': '==',
|
|
81
|
-
'<': '>=',
|
|
82
|
-
'>=': '<',
|
|
83
|
-
'>': '<=',
|
|
84
|
-
'<=': '>',
|
|
85
|
-
};
|
|
86
|
-
const f = table[cond.op];
|
|
87
|
-
if (f) {
|
|
88
|
-
return { k: 'bin', op: f, l: cond.l, r: cond.r };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return { k: 'un', op: '!', e: cond };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
73
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
95
74
|
* returns the rewritten block and the set live on entry. */
|
|
96
75
|
function dceBlock(
|
|
@@ -171,7 +150,7 @@ function dceBlock(
|
|
|
171
150
|
rev.push({ k: 'exprstmt', value: s.cond });
|
|
172
151
|
}
|
|
173
152
|
} else if (t.out.length === 0) {
|
|
174
|
-
rev.push({ k: 'if', cond:
|
|
153
|
+
rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
|
|
175
154
|
} else {
|
|
176
155
|
rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
|
|
177
156
|
}
|
package/src/l3/hoist.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// L3 — the naming MECHANISM shared by every pass that hoists a value into a fresh local.
|
|
2
|
+
//
|
|
3
|
+
// Two passes name bases today (`basecse.ts` hoists a REUSED base; `argbase.ts` names a call's
|
|
4
|
+
// argument bases), and they differ in POLICY — which bases are eligible, and when it is worth
|
|
5
|
+
// doing — but not in how a name is chosen. That half was copied, and the copy silently lost a
|
|
6
|
+
// safety guard: basecse added the callee-name exclusion in its own audit precisely so a hoist
|
|
7
|
+
// local could not shadow a called function, and the second implementation did not have it. A third
|
|
8
|
+
// hoisting pass would lose it again, so the mechanism lives here and the policy stays with each
|
|
9
|
+
// caller.
|
|
10
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
11
|
+
import { mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
12
|
+
|
|
13
|
+
/** Every identifier a hoist name must not collide with, anywhere in `sfn`.
|
|
14
|
+
*
|
|
15
|
+
* Wider than "the declared locals" on purpose, and each addition is a real collision:
|
|
16
|
+
* - params and locals, obviously;
|
|
17
|
+
* - every `var`/`addr` mentioned — a GLOBAL is referenced by bare name, so a local shadowing one
|
|
18
|
+
* silently redirects every later mention of it;
|
|
19
|
+
* - every CALL TARGET — a local named like a callee shadows the function;
|
|
20
|
+
* - every assignment target, which includes names no declaration list carries. */
|
|
21
|
+
function takenNames(sfn: SFn): Set<string> {
|
|
22
|
+
const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
23
|
+
const visit = (e: Expr): void => {
|
|
24
|
+
if (e.k === 'var' || e.k === 'addr') {
|
|
25
|
+
taken.add(e.name);
|
|
26
|
+
}
|
|
27
|
+
if (e.k === 'call') {
|
|
28
|
+
taken.add(e.fn);
|
|
29
|
+
}
|
|
30
|
+
mapExprChildren(e, (c) => {
|
|
31
|
+
visit(c);
|
|
32
|
+
return c;
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const walk = (stmts: Stmt[]): void => {
|
|
36
|
+
for (const s of stmts) {
|
|
37
|
+
if (s.k === 'assign') {
|
|
38
|
+
taken.add(s.name);
|
|
39
|
+
}
|
|
40
|
+
stmtExprs(s).forEach(visit);
|
|
41
|
+
walk(stmtChildren(s));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk(sfn.body);
|
|
45
|
+
return taken;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A generator of fresh `p<n>` hoist names for `sfn`, colliding with nothing already in it.
|
|
50
|
+
*
|
|
51
|
+
* Returned as a closure over one `taken` set so successive calls cannot collide with each OTHER
|
|
52
|
+
* either — the failure a caller re-deriving the set per name would hit.
|
|
53
|
+
*/
|
|
54
|
+
export function nameAllocator(sfn: SFn): () => string {
|
|
55
|
+
const taken = takenNames(sfn);
|
|
56
|
+
return () => {
|
|
57
|
+
let n = 0;
|
|
58
|
+
while (taken.has(`p${n}`)) {
|
|
59
|
+
n++;
|
|
60
|
+
}
|
|
61
|
+
const nm = `p${n}`;
|
|
62
|
+
taken.add(nm);
|
|
63
|
+
return nm;
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/l3/reindex.ts
CHANGED
|
@@ -163,10 +163,17 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
163
163
|
if (mentionsVar(e.idx, walk.p)) {
|
|
164
164
|
return null; // a p-dependent element offset — beyond the v1 shape
|
|
165
165
|
}
|
|
166
|
+
if (e.lead && e.lead.length > 0) {
|
|
167
|
+
return null; // leading subscripts (a multidim array global) — the rebuild below would drop
|
|
168
|
+
// them, turning an element access into a row's. Decline rather than reindex.
|
|
169
|
+
}
|
|
166
170
|
const idx: Expr =
|
|
167
171
|
e.idx.k === 'const' && e.idx.value === 0
|
|
168
172
|
? { k: 'var', name: iv }
|
|
169
173
|
: { 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. `lead` is
|
|
175
|
+
// declined above (the deref side); it cannot arrive on the base side either, since `walk.base`
|
|
176
|
+
// is a local pointer and structuring only ever puts `lead` on an array GLOBAL's own name.
|
|
170
177
|
return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
|
|
171
178
|
}
|
|
172
179
|
let failed = false;
|