@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.
Files changed (43) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +154 -5
  4. package/src/backend/cpp.ts +3 -1
  5. package/src/backend/pascal.ts +11 -0
  6. package/src/contracts.ts +37 -5
  7. package/src/declare.ts +251 -0
  8. package/src/frontend/frontend.ts +12 -2
  9. package/src/frontend/mips.ts +24 -23
  10. package/src/frontend/opaque.ts +39 -2
  11. package/src/frontend/ssa.ts +32 -53
  12. package/src/frontend/thumb.ts +420 -32
  13. package/src/ir/opcodes.ts +44 -0
  14. package/src/ir/simplify.ts +72 -0
  15. package/src/l3/argbase.ts +216 -0
  16. package/src/l3/ast.ts +126 -6
  17. package/src/l3/basecse.ts +3 -40
  18. package/src/l3/coalesce.ts +146 -0
  19. package/src/l3/dce.ts +2 -23
  20. package/src/l3/hoist.ts +65 -0
  21. package/src/l3/reindex.ts +7 -0
  22. package/src/l3/scopebase.ts +436 -0
  23. package/src/l3/symbol-refs.ts +61 -0
  24. package/src/l3/tailmerge.ts +120 -0
  25. package/src/l3/typing.ts +4 -0
  26. package/src/macros.ts +335 -0
  27. package/src/pattern/engine.ts +99 -6
  28. package/src/pipeline.ts +20 -6
  29. package/src/proto.ts +55 -0
  30. package/src/raise/divpow2.ts +226 -0
  31. package/src/raise/gvn.ts +141 -0
  32. package/src/raise/pre-recovery.ts +37 -3
  33. package/src/raise/recover.ts +24 -7
  34. package/src/raise/retsink.ts +36 -7
  35. package/src/raise/shortcircuit.ts +264 -22
  36. package/src/raise/structs.ts +12 -2
  37. package/src/rank.ts +370 -79
  38. package/src/structure/analysis.ts +42 -1
  39. package/src/structure/structure.ts +852 -67
  40. package/src/structure/switch-recover.ts +21 -3
  41. package/src/symbols.ts +541 -0
  42. package/src/target.ts +4 -2
  43. 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: negate(s.cond), then: e.out, else: [] });
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
  }
@@ -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;