@asmlift/core 0.7.0 → 0.8.1

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 (44) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/pascal.ts +2 -2
  4. package/src/codegen-flags.ts +640 -0
  5. package/src/frontend/disasm.ts +141 -11
  6. package/src/frontend/high-half.ts +149 -0
  7. package/src/frontend/mips.ts +458 -209
  8. package/src/frontend/ppc.ts +332 -67
  9. package/src/frontend/reloc-symbol.ts +109 -0
  10. package/src/frontend/splat.ts +56 -18
  11. package/src/frontend/ssa.ts +126 -29
  12. package/src/frontend/stackargs.ts +420 -0
  13. package/src/frontend/thumb.ts +207 -230
  14. package/src/ir/core.ts +62 -3
  15. package/src/ir/opcodes.ts +9 -0
  16. package/src/ir/parse.ts +7 -1
  17. package/src/l3/advance.ts +2 -2
  18. package/src/l3/argbase.ts +2 -2
  19. package/src/l3/argcopy.ts +269 -0
  20. package/src/l3/ast.ts +45 -1
  21. package/src/l3/basecse.ts +2 -2
  22. package/src/l3/coalesce.ts +109 -52
  23. package/src/l3/scopebase.ts +4 -4
  24. package/src/l3/tailret.ts +70 -0
  25. package/src/l3/unmerge.ts +2 -2
  26. package/src/l3/unreduce.ts +2 -1
  27. package/src/mangle.ts +49 -0
  28. package/src/pattern/engine.ts +128 -13
  29. package/src/pipeline.ts +22 -11
  30. package/src/raise/extscale.ts +5 -2
  31. package/src/raise/paramwidth.ts +111 -3
  32. package/src/raise/pre-recovery.ts +11 -1
  33. package/src/raise/retsink.ts +8 -4
  34. package/src/raise/tailsink.ts +17 -2
  35. package/src/rank-declare.ts +17 -9
  36. package/src/rank.ts +54 -20
  37. package/src/structure/retspell.ts +95 -0
  38. package/src/structure/structure.ts +12 -3
  39. package/src/structure/switch-recover.ts +1 -1
  40. package/src/target.ts +224 -14
  41. package/src/trace.ts +27 -18
  42. package/src/variation-definitions.ts +52 -2
  43. package/src/variation-gates.ts +3 -0
  44. package/src/variation-tokens.ts +1 -0
@@ -2,6 +2,29 @@
2
2
  // parses GNU-as text, not objdump, so it does not route through here.
3
3
  import { FrontendUnsupportedError } from './errors';
4
4
 
5
+ /** The objdump function-header line, exported through the two readers below so that nothing can
6
+ * disagree about where a function starts. GREEDY to the LAST `>`: a C++ template symbol contains
7
+ * `>` of its own (`invoke__Q23zen20NumberPicCallBack<i>FP7P2DPane`), and a header a pattern cannot
8
+ * see is worse than one it misreads — the PRECEDING function's slice runs on through it, and the
9
+ * prologue split swallows it. */
10
+ const HEADER_LINE = /^([0-9a-f]+)\s+<(.+)>:\s*$/i;
11
+ const HEADER_SEARCH = new RegExp(HEADER_LINE.source, 'im');
12
+
13
+ /** Character offset of the FIRST function header in an objdump listing, or -1 — everything before
14
+ * it is the listing's own prologue (`target.o: file format …`, section headings). */
15
+ export function firstFunctionHeader(disasm: string): number {
16
+ return disasm.search(HEADER_SEARCH);
17
+ }
18
+
19
+ /** The ADDRESS the first function header gives, or undefined for headerless input (a raw
20
+ * instruction fragment, which says where it starts only by its first line). It is the one thing
21
+ * that tells a word with nothing before it apart from a word the listing does not spell, and
22
+ * those two have opposite answers wherever what precedes a word decides how it may be read. */
23
+ export function symbolStart(disasm: string): number | undefined {
24
+ const m = disasm.match(HEADER_SEARCH);
25
+ return m ? parseInt(m[1], 16) : undefined;
26
+ }
27
+
5
28
  /** Slice a multi-symbol objdump listing down to ONE function's lines. objdump marks each
6
29
  * function with an `ADDR <sym>:` header line; when headers are present the input is sliced to
7
30
  * exactly the requested symbol — and an ABSENT symbol declines LOUD, because emitting some
@@ -11,9 +34,9 @@ export function sliceSymbol(disasm: string, symbol: string): string {
11
34
  const lines = disasm.split('\n');
12
35
  const headers: { line: number; sym: string }[] = [];
13
36
  for (let i = 0; i < lines.length; i++) {
14
- const m = lines[i].match(/^[0-9a-f]+\s+<([^>]+)>:\s*$/i);
37
+ const m = lines[i].match(HEADER_LINE);
15
38
  if (m) {
16
- headers.push({ line: i, sym: m[1] });
39
+ headers.push({ line: i, sym: m[2] });
17
40
  }
18
41
  }
19
42
  if (headers.length === 0) {
@@ -29,38 +52,136 @@ export function sliceSymbol(disasm: string, symbol: string): string {
29
52
  return lines.slice(headers[at].line, end).join('\n');
30
53
  }
31
54
 
55
+ /** A relocation objdump printed (with `-r`) under the instruction whose operand field it fills.
56
+ * All three fields carry meaning the rest of the listing does not:
57
+ * the TYPE says WHICH field — an `@ha` immediate half (`R_PPC_ADDR16_HA`), an `@l` half
58
+ * (`R_PPC_ADDR16_LO`), a small-data memory base (`R_PPC_EMB_SDA21`), a call target
59
+ * (`R_PPC_REL24`) — and the mnemonic cannot stand in for it; the ADDEND is part of the address,
60
+ * so `SYM` and `SYM+0x4` are different words; the SYMBOL is the name. */
61
+ export interface DisasmReloc {
62
+ type: string;
63
+ sym: string;
64
+ addend: number;
65
+ }
66
+
32
67
  /** One disassembled instruction. `target` is a decoded branch-target address (objdump prints the
33
- * target as `10 <sym+0x10>` in the last operand); `sym` is a relocation-attached callee symbol
34
- * (PPC `-r` output), absent otherwise. */
68
+ * target as `10 <sym+0x10>` in the last operand); `reloc` is the relocation objdump attached to
69
+ * this instruction (PPC `-r` output), absent otherwise. */
35
70
  export interface DisasmInstr {
36
71
  addr: number;
37
72
  mnemonic: string;
38
73
  ops: string[];
39
74
  target?: number;
40
- sym?: string;
75
+ reloc?: DisasmReloc;
41
76
  }
42
77
 
43
78
  export interface DisasmOptions {
44
79
  /** Attach relocation lines (`ADDR: R_* <sym>[+addend]`) to the PRECEDING instruction — the
45
- * callee symbol for a `bl` whose encoded offset is a 0 placeholder (PPC `-r` output). Tested
46
- * BEFORE the instruction regex, which would otherwise mis-read `R_PPC_…` as a mnemonic. */
80
+ * callee symbol for a `bl` whose encoded offset is a 0 placeholder, the named global behind a
81
+ * printed-as-0 immediate or memory base (PPC `-r` output). Tested BEFORE the instruction regex,
82
+ * which would otherwise mis-read `R_PPC_…` as a mnemonic. */
47
83
  relocs?: boolean;
48
84
  /** Strip branch-prediction hint suffixes glued onto the mnemonic (`blt-`, `bge+`, `bgelr-`).
49
85
  * The suffix is a prediction hint, not a different instruction — without stripping, the
50
86
  * mnemonic misses the cond tables and the branch is silently dropped. */
51
87
  hintSuffixes?: boolean;
88
+ /** What an all-zero word decodes to on this architecture (MIPS: `nop` — `sll zero,zero,0`).
89
+ * objdump prints a run of zero words as a bare `...` instead of the words themselves, and the
90
+ * words it stands for are real program text: a `mflo` hazard pad sits between a multiply and
91
+ * the branch that reads it. Without a decoding they cannot be recovered, so the listing is
92
+ * refused rather than parsed one word short (a zero word is not an instruction at all on
93
+ * PowerPC, whose frontend therefore supplies none). */
94
+ zeroWord?: string;
95
+ }
96
+
97
+ /** Fixed instruction width, in bytes, of every ISA that reaches this reader (MIPS, PowerPC), so an
98
+ * address gap is a whole number of words. Thumb is variable-width and has a reader of its own. */
99
+ const WORD = 4;
100
+
101
+ /** objdump's elision of a run of zero words: a bare `...` on its own line (`-d` prints it only
102
+ * for zeroes; `-z` prints the words instead). */
103
+ const ELISION_LINE = /^\s*\.\.\.\s*$/;
104
+
105
+ /** An instruction line's address column, whatever the mnemonic column holds. A line that carries
106
+ * an address carries a WORD of the function, so one this reader cannot decode must not be
107
+ * skipped: the words after it would keep their addresses while the list lost one, and every
108
+ * reader of that list — the delay slot at `branch + 4` above all — would be answering about a
109
+ * word that is not there. */
110
+ const ADDRESSED_LINE = /^\s*[0-9a-f]+:\s/i;
111
+
112
+ /** Replace an elision with the zero words it stands for: from the word after the last instruction
113
+ * parsed up to (not including) the address of the line that ends the run. Every way the run's
114
+ * extent is unknowable refuses by name — a run of unknown length is exactly the silent hole this
115
+ * exists to remove. A run with NO line after it is not a hole: it is the padding past the last
116
+ * word objdump printed, it bounds nothing, and nothing is invented for it. */
117
+ function expandElision(out: DisasmInstr[], next: number, zeroWord: string | undefined): void {
118
+ const prev = out[out.length - 1];
119
+ const hex = (a: number) => `0x${a.toString(16)}`;
120
+ if (!prev) {
121
+ throw new FrontendUnsupportedError(
122
+ `objdump elided a run of zero words ('...') before the first instruction of the listing, ` +
123
+ `ending at ${hex(next)}: where the run begins is unknown`,
124
+ );
125
+ }
126
+ const from = prev.addr + WORD;
127
+ if (zeroWord === undefined) {
128
+ throw new FrontendUnsupportedError(
129
+ `objdump elided a run of zero words ('...') at ${hex(from)}, but a zero word is not a ` +
130
+ `decodable instruction on this architecture`,
131
+ );
132
+ }
133
+ if (next <= from || (next - from) % WORD !== 0) {
134
+ throw new FrontendUnsupportedError(
135
+ `objdump elided a run of zero words ('...') between ${hex(from)} and ${hex(next)}, which is ` +
136
+ `not a whole number of instruction words`,
137
+ );
138
+ }
139
+ for (let addr = from; addr < next; addr += WORD) {
140
+ out.push({ addr, mnemonic: zeroWord, ops: [] });
141
+ }
142
+ }
143
+
144
+ /** Attach a parsed relocation to the instruction it belongs to. The binding is POSITIONAL —
145
+ * objdump prints a relocation directly beneath its instruction — and both ways that assumption
146
+ * can break fail LOUD, because each one silently relocates the wrong operand: an offset outside
147
+ * the preceding instruction's four bytes means the listing is not the assumed shape (the offset
148
+ * points at the relocated FIELD, so a 16-bit immediate's offset is the instruction's address + 2),
149
+ * and a second relocation on one instruction would overwrite the first, leaving one symbol
150
+ * standing for two. */
151
+ function attachReloc(out: DisasmInstr[], offset: number, reloc: DisasmReloc): void {
152
+ const ins = out[out.length - 1];
153
+ if (!ins || offset < ins.addr || offset >= ins.addr + 4) {
154
+ throw new FrontendUnsupportedError(
155
+ `relocation '${reloc.type} ${reloc.sym}' at 0x${offset.toString(16)} does not fall inside ` +
156
+ (ins ? `the preceding instruction ('${ins.mnemonic}' at 0x${ins.addr.toString(16)})` : 'any instruction'),
157
+ );
158
+ }
159
+ if (ins.reloc) {
160
+ throw new FrontendUnsupportedError(
161
+ `two relocations on one instruction ('${ins.mnemonic}' at 0x${ins.addr.toString(16)}): ` +
162
+ `'${ins.reloc.type} ${ins.reloc.sym}' and '${reloc.type} ${reloc.sym}'`,
163
+ );
164
+ }
165
+ ins.reloc = reloc;
52
166
  }
53
167
 
54
168
  /** Parse objdump `-d --no-show-raw-insn` output into a flat instruction list with addresses. */
55
169
  export function parseDisasm(disasm: string, opts: DisasmOptions = {}): DisasmInstr[] {
56
170
  const out: DisasmInstr[] = [];
171
+ let elided = false;
57
172
  for (const raw of disasm.split('\n')) {
173
+ if (ELISION_LINE.test(raw)) {
174
+ elided = true;
175
+ continue;
176
+ }
58
177
  if (opts.relocs) {
59
- const rel = raw.match(/^\s+[0-9a-f]+:\s+R_\w+\s+(\S+)/i);
178
+ const rel = raw.match(/^\s+([0-9a-f]+):\s+(R_\w+)\s+([^\s+-]+)(?:\s*([+-])\s*(0x[0-9a-f]+|\d+))?\s*$/i);
60
179
  if (rel) {
61
- if (out.length) {
62
- out[out.length - 1].sym = rel[1].split('+')[0];
63
- }
180
+ attachReloc(out, parseInt(rel[1], 16), {
181
+ type: rel[2],
182
+ sym: rel[3],
183
+ addend: rel[5] ? parseImm(rel[5]) * (rel[4] === '-' ? -1 : 1) : 0,
184
+ });
64
185
  continue;
65
186
  }
66
187
  }
@@ -68,9 +189,18 @@ export function parseDisasm(disasm: string, opts: DisasmOptions = {}): DisasmIns
68
189
  ? raw.match(/^\s*([0-9a-f]+):\s+([a-z][a-z0-9._]*)([-+]?)\s*(.*?)\s*$/i)
69
190
  : raw.match(/^\s*([0-9a-f]+):\s+([a-z][a-z0-9._]*)\s*(.*?)\s*$/i);
70
191
  if (!m) {
192
+ if (ADDRESSED_LINE.test(raw)) {
193
+ throw new FrontendUnsupportedError(
194
+ `objdump line '${raw.trim()}' carries an address but no instruction this reader can decode`,
195
+ );
196
+ }
71
197
  continue;
72
198
  }
73
199
  const addr = parseInt(m[1], 16);
200
+ if (elided) {
201
+ expandElision(out, addr, opts.zeroWord);
202
+ elided = false;
203
+ }
74
204
  const mnemonic = m[2]; // hint suffix (group 3), when parsed, is dropped
75
205
  const opsStr = opts.hintSuffixes ? m[4] : m[3];
76
206
  const ops = opsStr
@@ -0,0 +1,149 @@
1
+ // asmlift — the HIGH HALF of a relocated address, shared by the MIPS and PowerPC frontends.
2
+ //
3
+ // Both ISAs materialise a global's address in two instructions: a high-half producer (`lis
4
+ // rD,SYM@ha`, `lui rD,%hi(SYM)`) and a low-half consumer (`addi rD,rHi,SYM@l`, `lw rD,%lo(SYM)(rHi)`)
5
+ // that completes it. In a RELOCATABLE object neither printed immediate carries the address — it
6
+ // lives entirely in the two relocation records — so the high half is NOT A VALUE but a placeholder
7
+ // that means nothing until its matching low half completes it. Letting one reach the IR as an
8
+ // ordinary number hands the pipeline a plausible address that compiles and is simply wrong, so the
9
+ // invariant is absolute: a placeholder is either consumed by its low half or refuses.
10
+ //
11
+ // KEYED BY VALUE, NOT BY REGISTER, and the difference is the whole point. A register-keyed map
12
+ // answers "was a high half put in rX?", which is not the question; the question is "does the half
13
+ // reach THIS read?". Those differ whenever a redefinition on any path erases the entry, or a
14
+ // sibling path the reader never takes writes the register — and the register-keyed reading then
15
+ // falls through to whatever def reached before the producer. Keying by the SSA value makes the
16
+ // pairing a PROOF, answered by SSA rather than by a side map with an invalidation discipline every
17
+ // future writer must maintain.
18
+ //
19
+ // WHAT EACH ISA KEEPS FOR ITSELF is the ADDEND, and it is a relocation-format fact, not a
20
+ // preference. PowerPC objects are RELA: the addend rides on the relocation record. MIPS objects are
21
+ // REL: the record has no addend field and the value is split across the two instruction immediates
22
+ // (`(hi_imm << 16) + (s16)lo_imm`), so the MIPS fold computes the high half's contribution itself.
23
+ // Either way `addend` is "the part of the address this producer contributed", and the consumer adds
24
+ // its own.
25
+ import type { Block, Value } from '../ir/core';
26
+
27
+ /** A pending high half: what it names, what it contributed, and the instruction that produced it,
28
+ * so a refusal can point a reader at it. Whether it has been consumed is deliberately NOT here: it
29
+ * is this module's own bookkeeping, set by `pair`, so no frontend can fold a pair and forget to
30
+ * mark it — forgetting would refuse a well-formed function with nothing to catch it. */
31
+ export interface HighHalfInfo {
32
+ sym: string;
33
+ /** This producer's contribution to the address — RELA carries it, REL computes it. */
34
+ addend: number;
35
+ addr: number;
36
+ mnemonic: string;
37
+ }
38
+
39
+ /** How one ISA spells the two halves in a refusal, and how it fails loud. */
40
+ export interface HighHalfDialect {
41
+ /** The high-half marker as the ISA's asm spells it: `@ha` (PowerPC), `%hi` (MIPS). */
42
+ hi: string;
43
+ /** The indefinite article `hi` takes, so one shared sentence reads right in both ISAs' asm. */
44
+ hiArticle: string;
45
+ /** The low-half marker: `@l` (PowerPC), `%lo` (MIPS). */
46
+ lo: string;
47
+ /** The frontend's designed loud-failure signal (`PpcUnsupportedError`, `FrontendUnsupportedError`). */
48
+ fail(message: string): never;
49
+ }
50
+
51
+ export interface HighHalves {
52
+ /** Record the placeholder `v` as the high half `info`. The producer writes `v` as an ordinary SSA
53
+ * definition, which is what lets SSA answer the pairing question later. */
54
+ record(v: Value, info: HighHalfInfo): void;
55
+ /** THE PAIRING. `v` is what the low half's base register holds HERE; it must be the very
56
+ * placeholder a producer defined, and the two halves must name the same symbol (`alsoMatches`
57
+ * adds the per-ISA rest of the match — PowerPC's RELA addend). Anything else refuses, naming
58
+ * `reg` and what it really holds. The half is CONSUMED on the way out, which is why this is a
59
+ * call and not a public flag: the mark and the proof cannot come apart. */
60
+ pair(site: string, reg: string, v: Value, loSym: string, alsoMatches?: (hi: HighHalfInfo) => boolean): HighHalfInfo;
61
+ /** True when `v` is a placeholder — for a caller that must EXCLUDE one (a call's argument count
62
+ * must not treat a half parked in an argument register as an argument). */
63
+ has(v: Value): boolean;
64
+ /** Reading a register AS A VALUE. A register holding a high half is not one, so this refuses
65
+ * rather than handing back a plausible number standing for an address. */
66
+ guardRead(name: string, reg: string, v: Value): Value;
67
+ /** A high half no low half ever completed. Finishing the lift would SILENTLY DROP the address it
68
+ * was building — every low-half consumer a frontend does not model lands here, which is what
69
+ * keeps "not modelled" from turning into "not emitted". */
70
+ assertAllConsumed(name: string): void;
71
+ /** The LAST line of defence, because `guardRead` sees only the reads a frontend routes through
72
+ * it. SSA builds a block parameter's incoming arguments with reads of its own: at a merge of a
73
+ * path that holds a half and a path that does not, the read returns the PARAMETER — an ordinary
74
+ * value — while the placeholder is appended to the predecessor's successor arguments. So the
75
+ * finished function is checked once: a placeholder anywhere in the IR refuses. */
76
+ assertNoneEscaped(name: string, blocks: Block[]): void;
77
+ }
78
+
79
+ export function makeHighHalves(d: HighHalfDialect): HighHalves {
80
+ const halves = new Map<Value, { info: HighHalfInfo; consumed: boolean }>();
81
+ return {
82
+ record: (v, info) => void halves.set(v, { info, consumed: false }),
83
+ has: (v) => halves.has(v),
84
+ pair(site, reg, v, loSym, alsoMatches) {
85
+ const entry = halves.get(v);
86
+ const hi = entry && entry.info.sym === loSym && (alsoMatches?.(entry.info) ?? true) ? entry.info : undefined;
87
+ if (!hi) {
88
+ d.fail(
89
+ `${site} carries the '${d.lo}' half of '${loSym}' but ${reg} ` +
90
+ (entry
91
+ ? `holds the high half of '${entry.info.sym}'`
92
+ : `holds no high half here — a reused register, a missing '${d.hi}', or ${d.hiArticle} ` +
93
+ `'${d.hi}' that reaches this instruction only through a merge or a loop header, where ` +
94
+ `what the register holds is the block parameter standing for the join and not the half`) +
95
+ ` — this frontend will not guess at the pair`,
96
+ );
97
+ }
98
+ entry!.consumed = true;
99
+ return hi;
100
+ },
101
+ guardRead(name, reg, v) {
102
+ const hi = halves.get(v)?.info;
103
+ if (hi) {
104
+ d.fail(
105
+ `cannot lift '${name}': ${reg} holds the high half of '${hi.sym}' (the '${hi.mnemonic}' at ` +
106
+ `0x${hi.addr.toString(16)}) and is read as a value — only its matching '${d.lo}' half may consume it`,
107
+ );
108
+ }
109
+ return v;
110
+ },
111
+ assertAllConsumed(name) {
112
+ // Lowest address first, so a function with several dangling halves names the one a reader
113
+ // meets first in the listing rather than whichever the map happened to iterate to.
114
+ const dangling = [...halves.values()]
115
+ .filter((h) => !h.consumed)
116
+ .map((h) => h.info)
117
+ .sort((a, b) => a.addr - b.addr)[0];
118
+ if (dangling) {
119
+ d.fail(
120
+ `cannot lift '${name}': '${dangling.mnemonic}' at 0x${dangling.addr.toString(16)} carries the ` +
121
+ `'${d.hi}' half of '${dangling.sym}' and no modelled instruction consumes its '${d.lo}' half — ` +
122
+ `the address is never completed`,
123
+ );
124
+ }
125
+ },
126
+ assertNoneEscaped(name, blocks) {
127
+ const escaped = (v: Value): void => {
128
+ const hi = halves.get(v)?.info;
129
+ if (hi) {
130
+ d.fail(
131
+ `cannot lift '${name}': the high half of '${hi.sym}' (the '${hi.mnemonic}' at ` +
132
+ `0x${hi.addr.toString(16)}) reaches a merge with values that are not it — what the register ` +
133
+ `holds there is not a value this frontend can write down`,
134
+ );
135
+ }
136
+ };
137
+ for (const b of blocks) {
138
+ b.params.forEach(escaped);
139
+ for (const op of b.ops) {
140
+ op.operands.forEach(escaped);
141
+ op.results.forEach(escaped);
142
+ for (const s of op.successors) {
143
+ s.args.forEach(escaped);
144
+ }
145
+ }
146
+ }
147
+ },
148
+ };
149
+ }