@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
package/src/contracts.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  stmtLists,
17
17
  walkExprs,
18
18
  } from './l3/ast';
19
+ import { mentionedLocals } from './l3/mentions';
19
20
  import { declaredTypes, exprCType } from './l3/typing';
20
21
 
21
22
  export class ContractError extends Error {
@@ -277,10 +278,58 @@ export function assertLocalsWritten(sfn: SFn): void {
277
278
  }
278
279
  }
279
280
 
280
- /** Post-lever: every read of a MINTED local its ADDRESS being taken included must sit where
281
+ /** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
282
+ * produced.
283
+ *
284
+ * THE FAILURE THIS CATCHES is the mirror of `assertLocalsWritten` above, and the three contracts
285
+ * beside it do not see it. A pass that consumes a local — l3/unmerge.ts substituting a merge temp
286
+ * into the arms, l3/coalesce.ts folding two names into one, l3/inlinebase.ts deleting a
287
+ * const-address pointer — drops the name from `sfn.locals` on the strength of an in-pass count
288
+ * that it is no longer mentioned. If that count is ever wrong the result is not a loud variation
289
+ * error: it is a candidate handed to the compiler with an undeclared identifier. Normally that is
290
+ * a dropped candidate, but in the REAL tier the candidate is compiled inside the project's
291
+ * vendored translation unit, where an orphaned name that collides with a context symbol compiles
292
+ * and scores. Measured on the shape that inhabits it — `locals = [p]`, body `p = 0; v16 = 1;
293
+ * *p = v16;` — `assertResolved`, `assertDerefsTyped` and `assertLocalsWritten` all pass: the name
294
+ * is neither `?` nor `undefined`, it is well-typed, and the question the third one asks is the
295
+ * OPPOSITE one (read but never written).
296
+ *
297
+ * A DIFFERENTIAL, and that is what makes it safe to run on every respelled tree. "Every name the tree
298
+ * mentions is declared" is NOT the invariant and would refuse correct output everywhere:
299
+ * structure.ts spells a write to a scalar global as a bare `assign` whose name is declared in the
300
+ * project's headers and nowhere in the tree (`gBlendValue = v;` — 71 such occurrences across 22
301
+ * winning sources, per l3/unmerge.ts's own note), and `SFn.globals` is the symbol-map-shaped
302
+ * subset, not that population. So the check speaks only about names the tree ITSELF declared a
303
+ * moment ago and the pass then removed — a set with no legitimate inhabitant, because a pass that
304
+ * drops a declaration is asserting exactly this.
305
+ *
306
+ * `addr` counts, like everywhere else: `&v` names the object as surely as a read does. */
307
+ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
308
+ const kept = new Set(after.locals.map((l) => l.name));
309
+ const dropped = new Set(before.locals.map((l) => l.name).filter((n) => !kept.has(n)));
310
+ if (!dropped.size) {
311
+ return;
312
+ }
313
+ // `l3/mentions.ts`'s walk, not a third copy of the node vocabulary — this is the LOUD BACKSTOP
314
+ // for the mistake that predicate guards, so it is the last place that should own its own.
315
+ // Locals only, deliberately: no L3 respell variation drops `SFn.params` (`pruneDeadParams` is L1 block
316
+ // params, ir/simplify.ts), so a params arm here would be a refusal with no inhabitant.
317
+ const found = mentionedLocals(after.body, dropped);
318
+ if (found.size) {
319
+ throw new ContractError(
320
+ `a respell variation deleted the declaration of ${[...found]
321
+ .sort()
322
+ .map((n) => `'${n}'`)
323
+ .join(', ')} in '${after.name}' ` +
324
+ `while the tree still names ${found.size > 1 ? 'them' : 'it'} — an undeclared identifier in the candidate`,
325
+ );
326
+ }
327
+ }
328
+
329
+ /** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
281
330
  * that local's assignment has already run.
282
331
  *
283
- * THE failure a placing lever can ship, and the only one the byte differ rewards: a base local whose
332
+ * THE failure a placing variation can ship, and the only one the byte differ rewards: a base local whose
284
333
  * assignment does not reach a use is a DIFFERENT VARIABLE — C that compiles, scores, and can win
285
334
  * (the shape #106 shipped). `contracts.ts`'s `assertLocalsWritten` does not see it: it accumulates
286
335
  * reads and writes as SETS over the whole body, so a local assigned in one arm and read after the
@@ -288,16 +337,16 @@ export function assertLocalsWritten(sfn: SFn): void {
288
337
  *
289
338
  * Checked on the EMITTED tree rather than argued from the plan, because the plan is what a bug
290
339
  * would be in. `rank.ts`'s `respell` catches the throw and drops the candidate, so the wrong
291
- * answer becomes a reported lever error instead of a scored spelling.
340
+ * answer becomes a reported variation error instead of a scored spelling.
292
341
  *
293
342
  * IT LIVES HERE, beside `assertLocalsWritten`, because it has that check's population and that
294
- * check's call site: levers that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
343
+ * check's call site: respell variations that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
295
344
  * l3/nearbase.ts, l3/reindex.ts, l3/scopebase.ts, l3/argbase.ts — are the population that can
296
- * produce the failure, so the check belongs on every lever tree rather than on one lever's.
345
+ * produce the failure, so the check belongs on every respelled tree rather than on one variation's.
297
346
  *
298
- * Called ABSOLUTELY by the placing levers that put an init inside a nested list, each over its own
347
+ * Called ABSOLUTELY by the placing variations that put an init inside a nested list, each over its own
299
348
  * plan; everywhere else it is reached through `assertPlacementSurvives` below, which is a
300
- * DIFFERENTIAL — so a placement no lever's tree ever satisfied is not judged, and a lever that
349
+ * DIFFERENTIAL — so a placement no respelled tree ever satisfied is not judged, and a variation that
301
350
  * mints nothing is not judged at all.
302
351
  *
303
352
  * A nested list gets a COPY of the reaching set, so an assignment inside one arm does not count as
@@ -357,13 +406,13 @@ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): voi
357
406
  }
358
407
 
359
408
  /** The same guarantee across a re-spelling that MOVES statements over a placement another pass
360
- * already made — `rank.ts`'s statement shapes (`/initfirst`, `/pollguard`, `/pollread`), derived
361
- * onto every lever tree after the lever placed its defs, and the lever-on-lever compositions in
409
+ * already made — `rank.ts`'s stacked variations (`/initfirst`, `/pollguard`, `/pollread`), derived
410
+ * onto every respelled tree after the variation placed its defs, and the variation-on-variation compositions in
362
411
  * the same file where a def-moving pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
363
- * `reindexWalks`) runs on a tree a placing lever built. `pollReads` folds a materialized re-read
412
+ * `reindexWalks`) runs on a tree a placing variation built. `pollReads` folds a materialized re-read
364
413
  * back into a loop condition, which is exactly such a move.
365
414
  *
366
- * A DIFFERENTIAL, which is what makes it safe on every lever: the walk judges the reshaped tree
415
+ * A DIFFERENTIAL, which is what makes it safe on every variation: the walk judges the reshaped tree
367
416
  * only where it already described the unshaped one, so a placement it cannot model (a def inside
368
417
  * a loop body read earlier in the same body is assigned on every iteration but the first) is not
369
418
  * judged either way. `minted` may name a local `before` does not carry — a mover mints its own —
@@ -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
+ }