@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
@@ -4,6 +4,7 @@
4
4
  // for target→frontend dispatch.
5
5
  import type { Fn } from '../ir/core';
6
6
  import type { Prototypes } from '../proto';
7
+ import type { SymbolMap } from '../symbols';
7
8
  import type { TargetDescription } from '../target';
8
9
  import type { AsmData } from './asmdata';
9
10
  import type { AsmTextFormat } from './format';
@@ -17,6 +18,15 @@ export interface Frontend {
17
18
  /** decode one function's assembly into an L1 Fn. `prototypes` supplies callee arities
18
19
  * (and any other header facts the frontend needs); an empty map is valid. `asmData` is the
19
20
  * OPTIONAL Regime-B side-table (data-section jump tables + relocations); absent ⇒ a
20
- * dense-switch dispatch declines/loud-fails. */
21
- lift(name: string, asm: string, target: TargetDescription, prototypes: Prototypes, asmData?: AsmData): Fn;
21
+ * dense-switch dispatch declines/loud-fails. `symbols` is the OPTIONAL address→symbol map
22
+ * (symbols.ts); today only the Thumb frontend consumes it (numeric-pool promotion) — the
23
+ * MIPS/PPC objdump dialect already carries symbol names in the asm text. */
24
+ lift(
25
+ name: string,
26
+ asm: string,
27
+ target: TargetDescription,
28
+ prototypes: Prototypes,
29
+ asmData?: AsmData,
30
+ symbols?: SymbolMap,
31
+ ): Fn;
22
32
  }
@@ -19,7 +19,7 @@
19
19
  // compares (`sltu`/`sltiu`) lower to `icmp_ult`; recover types their operands u32 so the backend
20
20
  // re-emits `sltu` (the operator is the same `<` — the signedness lives in the operand types).
21
21
  import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
22
- import type { Opcode } from '../ir/opcodes';
22
+ import { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
23
23
  import { T } from '../ir/types';
24
24
  import type { Prototypes } from '../proto';
25
25
  import type { TargetDescription } from '../target';
@@ -47,19 +47,6 @@ const COND_Z: Record<string, Opcode> = {
47
47
  bgez: 'icmp_sge',
48
48
  };
49
49
  const COND_RR: Record<string, Opcode> = { beq: 'icmp_eq', bne: 'icmp_ne' };
50
- // Negated icmp opcode (for the `slt …; beqz` "branch when false" fold).
51
- const NEG_ICMP: Record<string, Opcode> = {
52
- icmp_slt: 'icmp_sge',
53
- icmp_sge: 'icmp_slt',
54
- icmp_sgt: 'icmp_sle',
55
- icmp_sle: 'icmp_sgt',
56
- icmp_ult: 'icmp_uge',
57
- icmp_uge: 'icmp_ult',
58
- icmp_ugt: 'icmp_ule',
59
- icmp_ule: 'icmp_ugt',
60
- icmp_eq: 'icmp_ne',
61
- icmp_ne: 'icmp_eq',
62
- };
63
50
 
64
51
  const isZero = (r: string) => r === 'zero' || r === '$0';
65
52
  // The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
@@ -604,6 +591,18 @@ export function lift(
604
591
  }
605
592
  return readVar(r, bi);
606
593
  };
594
+ // `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
595
+ //
596
+ // Keyed by the SSA VALUE the compare produced, never by its register. Keying by register is the
597
+ // bug: `slt v0,a0,a1; xori v0,v0,1; beqz v0,L` (a materialised `a0 >= a1` that a branch then
598
+ // tests — IDO's spelling, and a live benchmark row) redefines v0, and a register-keyed record
599
+ // folds the branch against the DEAD `slt`, silently emitting the INVERTED condition. A value
600
+ // cannot go stale that way: `condValue` resolves the branch's register to whatever value reaches
601
+ // it and looks THAT up, so a redefinition simply misses and the honest `icmp_eq(rX, 0)` is
602
+ // emitted. It also needs no invalidation discipline to be maintained by every future writer —
603
+ // which matters, because `write` is NOT the only path that redefines a register (`lui
604
+ // rD,%hi(SYM)` deliberately reassigns rD's meaning without it).
605
+ const cmpDef = new Map<Value, { opcode: string; lhs: Value; rhs: Value }>();
607
606
  const write = (r: string, v: Value) => {
608
607
  // Writing a register clears any pending `%hi` it held — the high-half address is gone once the
609
608
  // register is reassigned (e.g. `lw rHi, %lo(SYM)(rHi)` reuses the base as the load dest). A
@@ -626,13 +625,11 @@ export function lift(
626
625
  // DELIBERATELY (unlike divState): a cross-block `mult`/`mflo` pair has no observed inhabitant,
627
626
  // and the miss degrades to a LOUD opaque, never silence.
628
627
  let mulState: { rs: Value; rt: Value; signed: boolean } | null = null;
629
- // `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
630
- const cmpDef = new Map<string, { value: Value; opcode: string; lhs: Value; rhs: Value }>();
631
628
  const emitCmp = (opc: Opcode, d: string, lhs: Value, rhs: Value) => {
632
629
  const v = mkValue(T.unk(32));
633
630
  ops.push(mkOp(opc, { operands: [lhs, rhs], results: [v] }));
634
631
  write(d, v);
635
- cmpDef.set(d, { value: v, opcode: opc, lhs, rhs });
632
+ cmpDef.set(v, { opcode: opc, lhs, rhs });
636
633
  };
637
634
 
638
635
  const decode = (ins: Instr) => {
@@ -1053,7 +1050,7 @@ function condValue(
1053
1050
  ops: Op[],
1054
1051
  read: (r: string) => Value,
1055
1052
  constVal: (n: number) => Value,
1056
- cmpDef: Map<string, { value: Value; opcode: string; lhs: Value; rhs: Value }>,
1053
+ cmpDef: Map<Value, { opcode: string; lhs: Value; rhs: Value }>,
1057
1054
  ): Value {
1058
1055
  const mk = (opc: Opcode, l: Value, r: Value): Value => {
1059
1056
  const v = mkValue(T.unk(32));
@@ -1064,15 +1061,19 @@ function condValue(
1064
1061
  return mk(COND_RR[br.mnemonic], read(br.ops[0]), read(br.ops[1]));
1065
1062
  }
1066
1063
  // *z forms compare a register against zero — except beqz/bnez may fold a preceding `slt`.
1067
- const rs = br.ops[0];
1068
- const folded = cmpDef.get(rs);
1064
+ // Resolve the register to its reaching VALUE first: that is the fold's key (so a redefinition
1065
+ // between the compare and the branch misses instead of folding stale), and it is also what
1066
+ // routes the operand through `read`'s loud guards (sp / `%hi` / `gp` used as data) on every
1067
+ // path — the fold used to bypass them by never reading the register at all.
1068
+ const rsv = read(br.ops[0]);
1069
+ const folded = cmpDef.get(rsv);
1069
1070
  if (folded && br.mnemonic === 'bnez') {
1070
- return folded.value;
1071
+ return rsv;
1071
1072
  } // branch when slt is true
1072
1073
  if (folded && br.mnemonic === 'beqz') {
1073
- return mk(NEG_ICMP[folded.opcode], folded.lhs, folded.rhs);
1074
+ return mk(NEGATED_ICMP[folded.opcode], folded.lhs, folded.rhs);
1074
1075
  } // …when false
1075
- return mk(COND_Z[br.mnemonic], read(rs), constVal(0));
1076
+ return mk(COND_Z[br.mnemonic], rsv, constVal(0));
1076
1077
  }
1077
1078
 
1078
1079
  /** The MIPS-II / IDO frontend, registered for the `mips` target. */
@@ -7,6 +7,37 @@
7
7
  // This module owns only the POLICY (which token is the destination, which are register sources).
8
8
  // The frontend still owns the SSA plumbing (how to `read` a source and `write`/emit the result),
9
9
  // because that is block-local state the policy must not touch.
10
+ //
11
+ // ─── A NOTE ON ALTERNATIVE MNEMONIC SPELLINGS ──────────────────────────────────────────────────
12
+ //
13
+ // Every ISA here accepts more than one spelling for some instructions, and the frontends handle
14
+ // that in two DIFFERENT ways on purpose. Which one is right is decided by the operands, not by
15
+ // taste:
16
+ //
17
+ // * PURE SYNONYM — same operands, same semantics, different name. Normalise it in a name→name
18
+ // table at the parse site, so every consumer of the mnemonic sees one name. Thumb does this
19
+ // for `ldsh`/`ldrsh`, `ldsb`/`ldrsb`, `ldm`/`ldmfd`/`ldmia`, `stm`/`stmea`/`stmia`, which
20
+ // ARM DDI 0029G Figure 1-6 gives a single encoding apiece.
21
+ //
22
+ // The table is the right shape THERE because the mnemonic is read by more than the decode
23
+ // switch — Thumb's `classifyXfer` matches it to tell a return from an indirect jump, the
24
+ // `storeClass` below matches it to decide whether an unmodelled op may be skipped, and the
25
+ // instruction-size walk tests it for `bl`. An alias arm on one `case` fixes one of those.
26
+ //
27
+ // * EXTENDED MNEMONIC / PSEUDO-INSTRUCTION — different operand GRAMMAR, so no rename can
28
+ // express it. Give it its own decode arm. MIPS `move rD,rS` (2 operands) is `addu rD,rS,zero`
29
+ // (3); PPC `mr rD,rS` is `or rD,rS,rS`; PPC `slwi rD,rS,n` (3) is `rlwinm rD,rS,n,mb,me` (5,
30
+ // with mask fields computed from n). Routing these through a table would be a category error.
31
+ //
32
+ // There is deliberately NO shared alias helper. As of writing, MIPS and PPC have no pure-synonym
33
+ // gap at all — every `unmodelled instruction` decline they produce across the whole benchmark is
34
+ // a genuinely unmodelled opcode (`lwc1`, `fmuls`, `fctiwz`, `subfe`, …), not a spelling — so such
35
+ // a helper would have exactly one caller. The bar for extracting one is the bar this module itself
36
+ // met: several frontends hand-copying the same policy AND observed drift between the copies.
37
+ //
38
+ // One thing that IS shared, and should stay shared: `display` below. A frontend that normalises
39
+ // spellings must still REPORT the one its input actually used, or a decline sends the reader
40
+ // looking for an instruction their disassembly does not contain.
10
41
  import { FrontendUnsupportedError } from './errors';
11
42
 
12
43
  export interface OpaquePolicy {
@@ -27,6 +58,11 @@ export interface OpaquePolicy {
27
58
  storeClass?: RegExp;
28
59
  /** attribution for thrown declines: the function being lifted (optionally "+ site"). */
29
60
  context?: string;
61
+ /** How to SPELL the mnemonic in messages, when that differs from the name used to classify it.
62
+ * A frontend that normalises legacy spellings (Thumb `ldsh` -> `ldrsh`) classifies on the
63
+ * canonical name but must report the one the input file actually contains — otherwise a decline
64
+ * names an instruction the reader cannot find in their own .s. Defaults to `mnemonic`. */
65
+ display?: string;
30
66
  /** Mnemonics PROVABLY effect-free — or deliberately transparent (Thumb push/pop frame ops) —
31
67
  * in this ISA: the ONLY unmodelled no-destination instructions that may be skipped. Any other
32
68
  * no-destination unmodelled instruction THROWS: a side-effect-only instruction (swi, syscall,
@@ -54,13 +90,14 @@ export interface OpaqueDest {
54
90
  * reaches structuring as the sentinel `?` and trips `assertResolved` — the loud failure the
55
91
  * contract requires, instead of a stale/absent value surfacing as confidently-wrong source. */
56
92
  export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy): OpaqueDest | null {
93
+ const shown = policy.display ?? mnemonic;
57
94
  if (policy.storeClass?.test(mnemonic)) {
58
95
  // `context` names the function (and, where the ISA has addresses, the site) — this message
59
96
  // lands verbatim in annotate-mode stub headers, where an un-attributed decline is
60
97
  // unactionable in a multi-function run.
61
98
  const where = policy.context ? `cannot lift '${policy.context}': ` : '';
62
99
  throw new FrontendUnsupportedError(
63
- `${where}unmodelled store-class instruction '${mnemonic}' — a memory write cannot be skipped or degraded to a register opaque`,
100
+ `${where}unmodelled store-class instruction '${shown}' — a memory write cannot be skipped or degraded to a register opaque`,
64
101
  );
65
102
  }
66
103
  const norm = policy.normalize ?? ((s) => s);
@@ -71,7 +108,7 @@ export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy
71
108
  } // explicitly transparent for this ISA
72
109
  const where = policy.context ? `cannot lift '${policy.context}': ` : '';
73
110
  throw new FrontendUnsupportedError(
74
- `${where}unmodelled effect instruction '${mnemonic}' — no register destination to degrade, and skipping it would silently delete its effect`,
111
+ `${where}unmodelled effect instruction '${shown}' — no register destination to degrade, and skipping it would silently delete its effect`,
75
112
  );
76
113
  }
77
114
  if (policy.isZero?.(dst)) {
@@ -3,11 +3,20 @@
3
3
  // CFG (predecessors per block) and, per block, emits ops through `readVar`/`writeVar`; this
4
4
  // module materialises block-argument phis at joins and back-edges.
5
5
  //
6
+ // `preds` is an EDGE list, not a block list: it carries one entry per CFG edge, so a `switch_br`
7
+ // with several case values reaching one block appears there several times. Both readings are
8
+ // needed and they are not interchangeable — phi wiring wants the distinct predecessor BLOCKS (one
9
+ // value each), while the args it appends belong to the EDGES (every one of them). `distinctPreds`
10
+ // names the first; `appendSuccessorArg` walks the second. (ir/core.ts `predecessors` and
11
+ // structure.ts `predecessorBlocks` have the same duality, and structure.ts already dedups ad hoc
12
+ // at its two join sites.)
13
+ //
6
14
  // Protocol: create the builder, then fill blocks in index order. For each block, emit its
7
15
  // computation via read/writeVar, push its terminator op last (successors referencing
8
16
  // `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
9
17
  // blocks are filled, call `finish()` to remove trivial phis.
10
- import { Block, Fn, Successor, Value, mkValue, replaceAllUsesWith } from '../ir/core';
18
+ import { Block, Fn, Value, mkValue } from '../ir/core';
19
+ import { simplifyTrivialPhis } from '../ir/simplify';
11
20
  import { T } from '../ir/types';
12
21
 
13
22
  export interface SsaBuilder {
@@ -27,6 +36,7 @@ export interface SsaBuilder {
27
36
  finish(): void;
28
37
  }
29
38
 
39
+ /** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
30
40
  export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
31
41
  const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
32
42
  const fn: Fn = { name, blocks: irBlocks };
@@ -38,6 +48,9 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
38
48
  const phiBlock = new Map<Value, number>();
39
49
  const paramReg = new Map<Value, string>();
40
50
 
51
+ // `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
52
+ const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
53
+
41
54
  const writeVar = (reg: string, b: number, v: Value) => defs[b].set(reg, v);
42
55
  const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
43
56
 
@@ -55,7 +68,10 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
55
68
  incompletePhis[b].set(reg, phi);
56
69
  return phi;
57
70
  }
58
- const ps = preds[b];
71
+ // DISTINCT predecessor blocks: a switch_br reaching this block on several case values is one
72
+ // predecessor with several edges, and it supplies ONE value — counting the edges instead would
73
+ // manufacture a join (and a phi) where there is none.
74
+ const ps = distinctPreds(b);
59
75
  if (ps.length === 0) {
60
76
  // live-in with no predecessor: an incoming argument register → function parameter.
61
77
  const p = mkValue(T.unk(32));
@@ -75,16 +91,24 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
75
91
  return phi;
76
92
  };
77
93
  const addPhiOperands = (reg: string, b: number) => {
78
- for (const p of preds[b]) {
94
+ for (const p of distinctPreds(b)) {
79
95
  appendSuccessorArg(p, b, readVar(reg, p));
80
96
  }
81
97
  };
82
- // Append `arg` to predecessor p's terminator successor that targets block b.
98
+ // Append `arg` to EVERY successor edge of predecessor p that targets block b.
99
+ //
100
+ // A predecessor normally has one edge to a given successor, but a `switch_br` has as many as it
101
+ // has case values, and two cases sharing a body (`case 1: case 2:`) is ordinary C. Block args
102
+ // belong to the EDGE, so each of those edges needs its own copy: appending to just the first (a
103
+ // `find`) left the others short, while `preds` listing the block once per edge made the loop run
104
+ // k times and pile k copies onto that same first edge. Both halves of that — every edge, once per
105
+ // predecessor BLOCK — have to hold together, which is why they are fixed in one place.
83
106
  const appendSuccessorArg = (p: number, b: number, arg: Value) => {
84
107
  const term = irBlocks[p].ops[irBlocks[p].ops.length - 1];
85
- const s = term.successors.find((su) => su.block === irBlocks[b]);
86
- if (s) {
87
- s.args.push(arg);
108
+ for (const s of term.successors) {
109
+ if (s.block === irBlocks[b]) {
110
+ s.args.push(arg);
111
+ }
88
112
  }
89
113
  };
90
114
  const sealBlock = (b: number) => {
@@ -128,7 +152,7 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
128
152
  filled[b] = true;
129
153
  sealReadyBlocks();
130
154
  },
131
- finish: () => simplifyTrivialPhis(fn, phiBlock),
155
+ finish: () => simplifyTrivialPhis(fn, (p) => phiBlock.delete(p)),
132
156
  };
133
157
  }
134
158
 
@@ -167,48 +191,3 @@ export function abiSortEntryParams(
167
191
  }
168
192
  entry.params.sort((x, y) => rank(x) - rank(y));
169
193
  }
170
-
171
- // Remove block-parameters that are really trivial phis: those whose incoming operands (across
172
- // every predecessor edge, ignoring self-references from a back-edge) are all the same single
173
- // value. Such a parameter carries no join information — a loop-invariant register or a value
174
- // defined before the join — so it is replaced by that value and the corresponding argument
175
- // dropped from each predecessor's terminator. Iterated to fixpoint because removing one phi
176
- // can make another trivial.
177
- function simplifyTrivialPhis(fn: Fn, phiBlock: Map<Value, number>): void {
178
- const edgesTo = (b: Block): Successor[] => {
179
- const out: Successor[] = [];
180
- for (const pb of fn.blocks) {
181
- for (const op of pb.ops) {
182
- for (const s of op.successors) {
183
- if (s.block === b) {
184
- out.push(s);
185
- }
186
- }
187
- }
188
- }
189
- return out;
190
- };
191
- let changed = true;
192
- while (changed) {
193
- changed = false;
194
- for (const b of fn.blocks) {
195
- const incoming = edgesTo(b);
196
- for (let i = b.params.length - 1; i >= 0; i--) {
197
- const param = b.params[i];
198
- const operands = incoming.map((s) => s.args[i]);
199
- const distinct = [...new Set(operands.filter((v) => v !== param))];
200
- if (distinct.length !== 1) {
201
- continue;
202
- } // a genuine join (or unreachable) — keep it
203
- const v = distinct[0];
204
- replaceAllUsesWith(fn, param, v);
205
- b.params.splice(i, 1);
206
- for (const s of incoming) {
207
- s.args.splice(i, 1);
208
- }
209
- phiBlock.delete(param);
210
- changed = true;
211
- }
212
- }
213
- }
214
- }