@asmlift/core 0.1.0 → 0.2.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 CHANGED
@@ -28,15 +28,10 @@ const { source, diagnostics } = decompile('my_func', asm, MIPS_IDO);
28
28
  console.log(source); // s32 my_func(s32 a0) { ... }
29
29
  ```
30
30
 
31
- Input is **text**: GBA `.s` assembly for the ARM target both agbcc compiler output and
32
- pret-style project splits (`asm/nonmatchings/`: `thumb_func_start` macros, `_08xxxxxx` labels,
33
- `.4byte` literal pools) and `objdump -d --no-show-raw-insn`
34
- output for MIPS/PPC the format follows what each target's toolchain actually produces. The
35
- frontends classify the input first (`frontend/format.ts`): text that positively matches the
36
- _other_ format declines at the boundary naming both, instead of failing confusingly mid-decode.
37
- Multi-function input is sliced to the named symbol; an absent symbol declines with the list of
38
- symbols present. (`@asmlift/cli` additionally accepts ELF **object files** and runs the right
39
- objdump for you.)
31
+ Input is **text**, following what each target's toolchain produces:
32
+
33
+ - The ARM target reads GBA `.s`, produced by agbcc and pret-style project splits.
34
+ - The MIPS/PPC targets read `objdump -d --no-show-raw-insn` output and Splat-disassembled `.s`.
40
35
 
41
36
  ### `decompile(name, asm, target, opts?)`
42
37
 
@@ -84,20 +79,20 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
84
79
 
85
80
  ### Modules
86
81
 
87
- | Module | What it is |
88
- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
- | `ir/{types,core,opcodes,print,parse,verify}.ts` | MLIR-lite substrate: CFG of blocks + typed **block-arguments**, the typed opcode registry (`Opcode`, the one `effects` table DCE and hoist guards derive from), printer + parser (round-trip for L1/scalar types — see the domain note in parse.ts), verifier (arity/attrs/terminators/SSA dominance, located errors) |
90
- | `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table) |
91
- | `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
92
- | `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → struct-pointer → short-circuit; plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `errors.ts` (`RaiseUnsupportedError`) |
93
- | `structure/*.ts` | L2→L3 in four modules: `loops.ts` (natural-loop discovery), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
94
- | `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
95
- | `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
96
- | `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
97
- | `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
98
- | `rank.ts` | Pure candidate enumeration + `rankBy` (an injected score function ranks). @asmlift/cli's differ ranks through `rankBy`; the playground's wasm scorer consumes the same enumeration with its own async loop |
99
- | `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as data — no `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
100
- | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
82
+ | Module | What it is |
83
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `ir/{types,core,opcodes,print,parse,verify}.ts` | MLIR-lite substrate: CFG of blocks + typed **block-arguments**, the typed opcode registry (`Opcode`, the one `effects` table DCE and hoist guards derive from), printer + parser (round-trip for L1/scalar types — see the domain note in parse.ts), verifier (arity/attrs/terminators/SSA dominance, located errors) |
85
+ | `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `splat.ts` (Splat-dialect MIPS → objdump-shaped instrs), `format.ts` (input-format classification), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table) |
86
+ | `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
87
+ | `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → struct-pointer → short-circuit; plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `errors.ts` (`RaiseUnsupportedError`) |
88
+ | `structure/*.ts` | L2→L3 in four modules: `loops.ts` (natural-loop discovery), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
89
+ | `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
90
+ | `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
91
+ | `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
92
+ | `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
93
+ | `rank.ts` | Pure candidate enumeration + `rankBy` (an injected score function ranks). @asmlift/cli's differ ranks through `rankBy`; the playground's wasm scorer consumes the same enumeration with its own async loop |
94
+ | `target.ts` | `TargetDescription` (ABI + capabilities + compilerBehaviors as data — no `arch ==` in shared code); toolchain paths live in `@asmlift/toolchains` |
95
+ | `contracts.ts`, `proto.ts`, `mangle.ts` | Boundary contracts; prototype tables; the CodeWarrior mangler |
101
96
 
102
97
  Scoring and ranking live across the package seam in [`@asmlift/cli`](../cli/README.md):
103
98
  `score.ts` + `objdiff.ts` (toolchain compiles → in-process pinned `objdiff-wasm`, fail-closed)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
package/src/contracts.ts CHANGED
@@ -97,6 +97,17 @@ export function assertDerefsTyped(sfn: SFn): void {
97
97
  }
98
98
  }
99
99
  }
100
+ // A bare global ADDRESS `&SYM` under `+`/`-` is an ESCAPING interior pointer: C scales the byte
101
+ // offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
102
+ // inexact. A load/store base folds the offset byte-correctly (globalOf turns `&SYM + N` into an
103
+ // `index`/`field` node whose base is a bare `addr`, never an `addr` under a `bin`), so an `addr`
104
+ // reaching a `+`/`-` operand here escaped to a value context — flag it rather than emit wrong bytes.
105
+ if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
106
+ const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
107
+ if (addrSide) {
108
+ bad.push(`interior pointer arithmetic on the global address '&${addrSide.name}'`);
109
+ }
110
+ }
100
111
  // `!p` is legal C (pointer truthiness); `-p`/`~p` are not.
101
112
  if (e.k === 'un' && e.op !== '!' && ctype(e.e)?.kind === 'ptr') {
102
113
  bad.push(`pointer operand under unary '${e.op}'`);
package/src/detect.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  // asmlift — small pure helpers over raw asm TEXT (no parsing): shared by the CLI and the
2
2
  // web playground, which both need a function name before they can call `decompile`.
3
3
 
4
- /** Best-effort function-name detection: the objdump symbol header, else the `.globl` name,
5
- * else the first label. Returns undefined when the asm names nothing (caller asks the user). */
4
+ /** Best-effort function-name detection: the objdump symbol header, else Splat's `glabel` marker,
5
+ * else the `.globl` name, else the first label. Returns undefined when the asm names nothing
6
+ * (caller asks the user). Returns the FIRST function in multi-function input, matching how the
7
+ * objdump header case auto-selects. */
6
8
  export function detectName(asm: string): string | undefined {
7
9
  return (
8
10
  asm.match(/^[0-9a-f]+ <([\w.$]+)>:/m)?.[1] ??
11
+ asm.match(/^\s*glabel\s+([\w.$]+)/m)?.[1] ??
9
12
  asm.match(/^\s*\.globl\s+([\w.$]+)/m)?.[1] ??
10
13
  asm.match(/^([A-Za-z_]\w*):/m)?.[1]
11
14
  );
@@ -9,12 +9,14 @@
9
9
  // hand-written instructions) stays "unknown" and flows through to the frontend — the
10
10
  // decode-level loud-fail nets still own that case.
11
11
  import { FrontendUnsupportedError } from './errors';
12
+ import { isSplatMips } from './splat';
12
13
 
13
- export type AsmTextFormat = 'objdump' | 'gnu-as';
14
+ export type AsmTextFormat = 'objdump' | 'gnu-as' | 'splat';
14
15
 
15
16
  const FORMAT_LABEL: Record<AsmTextFormat, string> = {
16
17
  objdump: 'objdump disassembly (`objdump -d --no-show-raw-insn` output)',
17
18
  'gnu-as': 'GNU-as assembly text (compiler-emitted `.s`)',
19
+ splat: 'Splat-disassembled MIPS (`glabel` + `/* rom vram bytes */` `.s`)',
18
20
  };
19
21
 
20
22
  // objdump output: `ADDR <sym>:` section headers, address-prefixed instruction lines, or the
@@ -23,8 +25,13 @@ const OBJDUMP_SIGNAL = /^[0-9a-f]{2,} <[^>]+>:|^\s+[0-9a-f]+:\t|file format /im;
23
25
  const GNU_AS_SIGNAL =
24
26
  /^\s*\.(text|code|align|globl|global|thumb_func|section|syntax|arch|cpu|set|ent|type|size|file)\b/im;
25
27
 
26
- /** Classify assembly TEXT by positive signals; "unknown" when neither (or both) match. */
28
+ /** Classify assembly TEXT by positive signals; "unknown" when neither (or both) match. Splat is
29
+ * checked FIRST: its files also carry GNU-as directives (`.section`/`.align`), so a bare gnu-as
30
+ * match would mislabel them — the Splat-specific `glabel`/instruction-comment signal is decisive. */
27
31
  export function classifyAsmText(text: string): AsmTextFormat | 'unknown' {
32
+ if (isSplatMips(text)) {
33
+ return 'splat';
34
+ }
28
35
  const objdump = OBJDUMP_SIGNAL.test(text);
29
36
  const gnuAs = GNU_AS_SIGNAL.test(text);
30
37
  if (objdump === gnuAs) {
@@ -42,6 +49,7 @@ export function assertInputFormat(frontendId: string, expected: AsmTextFormat, a
42
49
  }
43
50
  throw new FrontendUnsupportedError(
44
51
  `cannot lift: input looks like ${FORMAT_LABEL[got]}, but the '${frontendId}' frontend reads ` +
45
- `${FORMAT_LABEL[expected]} — MIPS/PPC targets take objdump output; the ARM/agbcc target takes agbcc .s text`,
52
+ `${FORMAT_LABEL[expected]} — the ARM/agbcc target takes agbcc .s text; MIPS/PPC take objdump output ` +
53
+ `(the MIPS frontend also reads Splat .s)`,
46
54
  );
47
55
  }
@@ -30,6 +30,7 @@ import { FrontendUnsupportedError } from './errors';
30
30
  import { assertInputFormat } from './format';
31
31
  import type { Frontend } from './frontend';
32
32
  import { opaqueDest } from './opaque';
33
+ import { isSplatMips, parseSplatMips } from './splat';
33
34
  import { abiSortEntryParams } from './ssa';
34
35
  import { makeSsaBuilder } from './ssa';
35
36
 
@@ -87,6 +88,99 @@ const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins);
87
88
  // `sll`+`addu` before the access, so no `base+index` addressing form appears in parseMem input.
88
89
  const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm);
89
90
 
91
+ // A MIPS `%hi`/`%lo` relocation operand — the assembler's HI16/LO16 split that materialises the
92
+ // address of a named global: `%hi(SYM)`, `%lo(SYM)`, `%hi(SYM + N)`, or the memory form
93
+ // `%lo(SYM + N)(base)`. Splat spells global access this way and the Splat parser preserves it
94
+ // verbatim (frontend/splat.ts); the objdump dialect hides the symbol in a relocation, which
95
+ // `applyMipsGlobalRelocs` rewrites into the same `%hi`/`%lo` operands. Either way `lift` folds a
96
+ // `lui %hi` + its consuming `%lo` into a single `gaddr(SYM)` (the op the Thumb frontend also emits
97
+ // for a pool-loaded global), carrying the addend as the access offset. Returns null for a
98
+ // non-`%hi/%lo` operand.
99
+ function parseReloc(kind: 'hi' | 'lo', operand: string): { sym: string; addend: number; base?: string } | null {
100
+ const m = operand.match(
101
+ new RegExp(String.raw`^%${kind}\(\s*([A-Za-z_.$][\w.$]*)\s*(?:\+\s*(0x[0-9a-fA-F]+|\d+))?\s*\)(?:\((\w+)\))?$`),
102
+ );
103
+ if (!m) {
104
+ return null;
105
+ }
106
+ return { sym: m[1], addend: m[2] ? parseImm(m[2]) : 0, base: m[3] };
107
+ }
108
+
109
+ // Bridge objdump global-access relocations into the `%hi`/`%lo` operands `parseReloc` reads, so the
110
+ // gaddr recognition recovers named globals from an object file the same way it does from Splat text.
111
+ // In objdump a global load shows `lui rX,0x0` with the symbol ONLY in the `R_MIPS_HI16`/`LO16`
112
+ // reloc records — without this the base decodes as address 0 and the access reads `*(T *)0`. Using
113
+ // asmData, rewrite each `lui`'s immediate to `%hi(SYM)` and its paired consumer's operand to
114
+ // `%lo(SYM[+N])`. Mirrors the harness's disasmToM2c rewrite, so asmlift and m2c recover the same
115
+ // symbols. NAMED object symbols only — a reloc against a `.rodata`/`.data` SECTION is a jump-table
116
+ // base (Regime B) or section-relative data, left untouched.
117
+ function applyMipsGlobalRelocs(instrs: Instr[], ad: AsmData): void {
118
+ const byAddr = new Map(instrs.map((ins) => [ins.addr, ins]));
119
+ const his: { addr: number; sym: string }[] = [];
120
+ const los = new Map<number, string>(); // LO16 instruction addr → symbol
121
+ for (const r of ad.relocs) {
122
+ if (r.section !== '.text' || r.sym.startsWith('.')) {
123
+ continue; // section-symbol relocs are jump tables / anonymous data — not named globals
124
+ }
125
+ if (r.type === 'R_MIPS_HI16') {
126
+ his.push({ addr: r.offset, sym: r.sym });
127
+ } else if (r.type === 'R_MIPS_LO16') {
128
+ los.set(r.offset, r.sym);
129
+ }
130
+ }
131
+ if (his.length === 0) {
132
+ return;
133
+ }
134
+ his.sort((a, b) => a.addr - b.addr);
135
+ const loAddrs = [...los.keys()].sort((a, b) => a - b);
136
+ const consumed = new Set<number>();
137
+ for (const hi of his) {
138
+ const lui = byAddr.get(hi.addr);
139
+ if (!lui || lui.mnemonic !== 'lui') {
140
+ continue;
141
+ }
142
+ // Pair with the first not-yet-consumed same-symbol LO16 after the lui (GCC emits the pair with
143
+ // the base register threaded, so a 1:1 by-symbol-and-order match is the observed shape).
144
+ const loAddr = loAddrs.find((a) => a > hi.addr && !consumed.has(a) && los.get(a) === hi.sym);
145
+ const lo = loAddr !== undefined ? byAddr.get(loAddr) : undefined;
146
+ if (!lo) {
147
+ continue;
148
+ }
149
+ // The addend N rides in the instruction fields, not the reloc record: `(HI16 imm << 16) + the
150
+ // LO16 instruction's signed immediate`. HI16 imm is 0 in a relocatable object.
151
+ const rw = rewriteLoReloc(lo, hi.sym, parseImm(lui.ops[1] ?? '0') << 16);
152
+ if (rw === null) {
153
+ continue; // an unmodelled consumer (FP load, …) — leave the pair raw; it declines downstream
154
+ }
155
+ lui.ops[1] = rw.n === 0 ? `%hi(${hi.sym})` : `%hi(${hi.sym} + 0x${rw.n.toString(16)})`;
156
+ consumed.add(loAddr!);
157
+ }
158
+ }
159
+
160
+ // Rewrite a LO16 consumer's operand to `%lo(SYM[+N])`, returning the addend N, or null when the
161
+ // instruction is not a modelled global consumer (leave it raw). `hiBase` is the HI16 imm << 16.
162
+ function rewriteLoReloc(lo: Instr, sym: string, hiBase: number): { n: number } | null {
163
+ const macro = (n: number) => (n === 0 ? `%lo(${sym})` : `%lo(${sym} + 0x${n.toString(16)})`);
164
+ if (lo.mnemonic === 'addiu' || lo.mnemonic === 'addi') {
165
+ const n = hiBase + parseImm(lo.ops[2] ?? '0');
166
+ if (n < 0) {
167
+ return null; // a negative interior offset — unusual; leave raw
168
+ }
169
+ lo.ops[2] = macro(n);
170
+ return { n };
171
+ }
172
+ if (/^(lw|lh|lhu|lb|lbu|sw|sh|sb)$/.test(lo.mnemonic)) {
173
+ const mem = parseMem(lo.ops[lo.ops.length - 1] ?? '');
174
+ const n = hiBase + mem.off;
175
+ if (n < 0) {
176
+ return null;
177
+ }
178
+ lo.ops[lo.ops.length - 1] = `${macro(n)}(${mem.base})`;
179
+ return { n };
180
+ }
181
+ return null;
182
+ }
183
+
90
184
  interface MipsBlock {
91
185
  startAddr: number;
92
186
  body: Instr[]; // computation instructions (excludes the branch and its delay slot)
@@ -370,8 +464,15 @@ export function lift(
370
464
  _prototypes: Prototypes = {},
371
465
  asmData?: AsmData,
372
466
  ): Fn {
373
- assertInputFormat('mips', 'objdump', asm);
374
- const instrs = parseDisasm(sliceSymbol(asm, name)); // ONE function only an absent symbol declines loud
467
+ // Two input dialects reach this frontend: `objdump -d` text (IDO/KMC — no compiler-emitted asm),
468
+ // and Splat-disassembled `.s` (pmret-style N64 projects). Splat is normalised to the SAME
469
+ // DisasmInstr[] shape (frontend/splat.ts) so everything below is dialect-agnostic.
470
+ const splat = isSplatMips(asm);
471
+ if (!splat) {
472
+ assertInputFormat('mips', 'objdump', asm);
473
+ }
474
+ // ONE function only — an absent symbol declines loud (either dialect's slicer enforces this).
475
+ const instrs = splat ? parseSplatMips(asm, name) : parseDisasm(sliceSymbol(asm, name));
375
476
  if (instrs.length === 0) {
376
477
  throw new FrontendUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
377
478
  }
@@ -380,6 +481,12 @@ export function lift(
380
481
  // exempted from the loud-fail below; an UNrecovered `jr <non-ra>` still fails loud.
381
482
  const jts = asmData ? recoverMipsJumpTables(instrs, asmData) : new Map<number, MipsJT>();
382
483
  const recoveredJr = new Set([...jts.values()].map((j) => j.jrAddr));
484
+ // Bridge global-access relocations into `%hi`/`%lo` operands (objdump dialect only — Splat text
485
+ // already carries them). Runs AFTER jump-table recovery so its raw `.rodata` table-base relocs are
486
+ // read pristine; global rewrites target NAMED symbols and never touch a jump-table base.
487
+ if (!splat && asmData) {
488
+ applyMipsGlobalRelocs(instrs, asmData);
489
+ }
383
490
  // TRUSTWORTHINESS: fail LOUD on a control transfer this frontend cannot model — the `opaque`
384
491
  // path cannot catch these (implicit or no register destination). `jal`/`jalr` clobber `v0`
385
492
  // implicitly, so dropping a call fabricates `v0` from a stale value; a `jr` to anything but `ra`
@@ -451,10 +558,31 @@ export function lift(
451
558
 
452
559
  const fillBlock = (b: MipsBlock, bi: number) => {
453
560
  const ops = irBlocks[bi].ops;
561
+ // Pending `lui rX, %hi(SYM)` relocations awaiting their consuming `%lo` (a load/store base or an
562
+ // `addiu`). Block-local: the pair is emitted adjacently, so a `%lo` with no matching in-scope
563
+ // `%hi` (a cross-block or gp-relative access) declines LOUD rather than fabricating a base.
564
+ const hiReloc = new Map<string, { sym: string; addend: number }>();
565
+ // Materialise the address of a named global — the same `gaddr` op the Thumb frontend emits; the
566
+ // structurer lowers a load/store through it to `SYM` (scalar) or `((T *)&SYM)[i]` (aggregate).
567
+ const emitGaddr = (sym: string): Value => {
568
+ const g = mkValue(T.unk(32));
569
+ ops.push(mkOp('gaddr', { results: [g], attrs: { sym } }));
570
+ return g;
571
+ };
454
572
  const read = (r: string): Value => {
455
573
  if (isZero(r)) {
456
574
  return constVal(0);
457
575
  }
576
+ // A `%hi(SYM)` register read as DATA before its `%lo` completes the address is a split hi/lo
577
+ // relocation (the high half used alone) this frontend does not model — decline rather than
578
+ // treat the partial address as a value. The legit consumers (load/store/addiu `%lo`) validate
579
+ // `hiReloc` directly and never route the base through `read`, so this fires only on misuse.
580
+ const hr = hiReloc.get(r);
581
+ if (hr) {
582
+ throw new FrontendUnsupportedError(
583
+ `cannot lift '${name}': %hi(${hr.sym}) register used as data before a matching %lo — split hi/lo relocation not modelled`,
584
+ );
585
+ }
458
586
  // Reading `sp` as a DATA operand means frame-pointer arithmetic or an address-taken local
459
587
  // (`addiu a0,sp,8` = `&local`) — not modellable without a stack abstraction. Fabricating a
460
588
  // value for `sp` invents a PHANTOM leading parameter that shifts every real argument — a
@@ -477,6 +605,10 @@ export function lift(
477
605
  return readVar(r, bi);
478
606
  };
479
607
  const write = (r: string, v: Value) => {
608
+ // Writing a register clears any pending `%hi` it held — the high-half address is gone once the
609
+ // register is reassigned (e.g. `lw rHi, %lo(SYM)(rHi)` reuses the base as the load dest). A
610
+ // `%hi` NOT overwritten persists across multiple `%lo` uses (the read-modify-write idiom).
611
+ hiReloc.delete(r);
480
612
  if (!isZero(r)) {
481
613
  writeVar(r, bi, v);
482
614
  }
@@ -527,6 +659,22 @@ export function lift(
527
659
  // and raise/const.ts folds the const/const pair into one 32-bit const — the form that
528
660
  // recompiles to this exact `lui;ori`.
529
661
  case 'lui':
662
+ // `lui rD, %hi(SYM)` is the high half of a global's address — record it, pending the `%lo`
663
+ // that completes it (below), instead of materialising a bogus numeric const. rD's SSA value
664
+ // is deliberately NOT written: the high half is meaningless alone, so the `gaddr` is emitted
665
+ // at the consuming `%lo`. A read of rD as data before that is caught by the `read` guard;
666
+ // an UNconsumed `%hi` (no `%lo`) is a dead `lui` whose rD is never read — the residual case
667
+ // (an unconsumed `%hi` reg read via a `readVar` bypass) does not occur in compiler output.
668
+ if (s.startsWith('%')) {
669
+ const hi = parseReloc('hi', s);
670
+ if (!hi) {
671
+ throw new FrontendUnsupportedError(`cannot lift '${name}': unsupported relocation immediate '${s}'`);
672
+ }
673
+ if (!isZero(d)) {
674
+ hiReloc.set(d, { sym: hi.sym, addend: hi.addend });
675
+ }
676
+ break;
677
+ }
530
678
  write(d, constVal((parseImm(s) << 16) >> 0));
531
679
  break;
532
680
  case 'addiu':
@@ -537,6 +685,24 @@ export function lift(
537
685
  if (isStackPtr(d)) {
538
686
  break;
539
687
  }
688
+ // `addiu rD, rHi, %lo(SYM)` completes a global's address materialised by a `lui %hi(SYM)`:
689
+ // rD = &SYM (+ addend for a byte offset into the global). Emits the shared `gaddr` op.
690
+ if (t.startsWith('%')) {
691
+ const lo = parseReloc('lo', t);
692
+ const hr = lo ? hiReloc.get(s) : undefined;
693
+ if (!lo || !hr || hr.sym !== lo.sym || hr.addend !== lo.addend) {
694
+ throw new FrontendUnsupportedError(
695
+ `cannot lift '${name}': %lo relocation '${t}' with no matching in-scope %hi — split/cross-block hi/lo not modelled`,
696
+ );
697
+ }
698
+ // `&SYM` (addend 0), or `&SYM + N` for a byte offset into the global. The `add` tree is
699
+ // folded byte-correctly by memAccess when this address is a load/store base; if it
700
+ // instead ESCAPES as a value, `assertDerefsTyped` declines it (the byte offset would
701
+ // element-scale in C) — see the interior-global-pointer guard there.
702
+ const g = emitGaddr(lo.sym);
703
+ lo.addend !== 0 ? emitBin('add', d, g, constVal(lo.addend)) : write(d, g);
704
+ break;
705
+ }
540
706
  if (isZero(s)) {
541
707
  write(d, constVal(parseImm(t)));
542
708
  break;
@@ -706,6 +872,16 @@ export function lift(
706
872
  // drop its destination register — emit an honest `opaque`: dead ⇒ it vanishes; live ⇒
707
873
  // assertResolved fails LOUD (see frontend/opaque.ts for the policy).
708
874
  const emitOpaqueDest = (ins: Instr) => {
875
+ // A `%hi`/`%lo` operand on an instruction NOT modelled as a global consumer — an FP load/store
876
+ // (`lwc1`/`ldc1`), or any unmodelled op — reaches here (the modelled consumers handle their own
877
+ // `%hi`/`%lo` and return before the default case). Dropping it to an opaque would silently
878
+ // delete the global access (its base is not a bare register the opaque srcReg scan can see), so
879
+ // decline LOUD rather than lose it.
880
+ if (ins.ops.some((o) => o.startsWith('%'))) {
881
+ throw new FrontendUnsupportedError(
882
+ `cannot lift '${name}': unmodelled instruction '${ins.mnemonic}' with a %hi/%lo global operand — not modelled`,
883
+ );
884
+ }
709
885
  // storeClass: unmodelled MIPS stores — incl. the unaligned pair swl/swr and the FPU stores,
710
886
  // whose FIRST token is a register (a SOURCE, not a dest) that would otherwise fabricate an
711
887
  // opaque write to it while dropping the real memory write.
@@ -730,7 +906,30 @@ export function lift(
730
906
  };
731
907
  const emitUn = kit.un;
732
908
  const emitShImm = (opc: Opcode, d: string, x: Value, sa: string) => kit.shImm(opc, d, x, parseImm(sa));
909
+ // Resolve a `%lo(SYM + N)(rHi)` memory operand to a global address: validate it pairs with an
910
+ // in-scope `%hi`, emit the `gaddr`, and return it as the base with the addend as the access
911
+ // offset. A non-`%lo` operand returns null (the caller falls through to the normal off(base)).
912
+ const globalBase = (mem: string): { base: Value; off: number } | null => {
913
+ if (!mem.startsWith('%')) {
914
+ return null;
915
+ }
916
+ const lo = parseReloc('lo', mem);
917
+ const hr = lo && lo.base ? hiReloc.get(lo.base) : undefined;
918
+ if (!lo || !lo.base || !hr || hr.sym !== lo.sym || hr.addend !== lo.addend) {
919
+ throw new FrontendUnsupportedError(
920
+ `cannot lift '${name}': %lo access '${mem}' with no matching in-scope %hi — split/cross-block hi/lo not modelled`,
921
+ );
922
+ }
923
+ return { base: emitGaddr(lo.sym), off: lo.addend };
924
+ };
733
925
  const emitLoad = (d: string, mem: string, width: number, signed: boolean) => {
926
+ const g = globalBase(mem);
927
+ if (g) {
928
+ const res = mkValue(T.unk(32));
929
+ ops.push(mkOp('load', { operands: [g.base], results: [res], attrs: { off: g.off, width, signed } }));
930
+ write(d, res);
931
+ return;
932
+ }
734
933
  const { off, base } = parseMem(mem);
735
934
  // A word reload from a stack slot is transparent to dataflow — the SAME value spilled — so
736
935
  // route it through the slot SSA variable, not a `load` through `sp` (which would make `sp` a
@@ -756,6 +955,11 @@ export function lift(
756
955
  write(d, res);
757
956
  };
758
957
  const emitStore = (srcReg: string, mem: string, width: number) => {
958
+ const g = globalBase(mem);
959
+ if (g) {
960
+ ops.push(mkOp('store', { operands: [g.base, read(srcReg)], attrs: { off: g.off, width } }));
961
+ return;
962
+ }
759
963
  const { off, base } = parseMem(mem);
760
964
  // A word spill to a stack slot (an argument home slot `sw a0,0(sp)`, or a local): record the
761
965
  // slot's value in SSA, do NOT emit a `store` through `sp`. A never-reloaded spill (the ABI
@@ -0,0 +1,305 @@
1
+ // asmlift — the Splat-dialect MIPS reader. Splat (the N64 disassembler that pmret/decomp.me-style
2
+ // projects run) emits a GNU-as flavour that neither `objdump -d` nor a compiler produce, so the
3
+ // shared objdump scaffolding (frontend/disasm.ts) reads nothing from it. This module normalises
4
+ // that dialect into the SAME `DisasmInstr[]` the objdump parser yields, so the whole MIPS frontend
5
+ // (delay slots, blocks, SSA, recovery) runs downstream unchanged — mirroring how the Thumb frontend
6
+ // grew a second dialect for pret/luvdis splits.
7
+ //
8
+ // What the dialect adds over objdump:
9
+ // • `glabel NAME` / `endlabel NAME` function markers (objdump uses `ADDR <sym>:` headers);
10
+ // • a `/* ROM VRAM BYTES */` comment prefix on every instruction (the addr lives INSIDE it, so
11
+ // disasm.ts's `ADDR:` line anchor never matches) — the VRAM word is the instruction address;
12
+ // • `$`-prefixed registers (`$v0`, `$sp`) — stripped to the bare names the frontend's guards expect;
13
+ // • `.L<vram>_<rom>` local labels as branch/jump TARGETS (objdump prints a resolved address) —
14
+ // resolved here to the target instruction's address;
15
+ // • constant immediate EXPRESSIONS (`(0x660104 >> 16)`, `(x & 0xFFFF)`) — the assembler's hi/lo
16
+ // split of a 32-bit literal, evaluated here to the plain number the decode switch parses.
17
+ //
18
+ // `%hi`/`%lo` operands (a global's address) are preserved verbatim so the MIPS frontend can fold
19
+ // them into a `gaddr` (frontend/mips.ts). The other GOT/PIC relocations (`%gp_rel`, `%got`, …) are
20
+ // declined LOUD — small-data / position-independent access is not modelled. Preserving rather than
21
+ // blindly evaluating is what keeps `parseImm('%hi(SYM)')` from silently becoming a NaN immediate.
22
+ import type { DisasmInstr } from './disasm';
23
+ import { FrontendUnsupportedError } from './errors';
24
+
25
+ // One instruction line: `/* ROM VRAM BYTES */ MNEMONIC OPS`. Group 1 is the VRAM address word.
26
+ const INSN_LINE = /^\/\*\s*[0-9A-Fa-f]+\s+([0-9A-Fa-f]+)\s+[0-9A-Fa-f]+\s*\*\/\s*(\S+)\s*(.*)$/;
27
+ // A Splat instruction-comment prefix anywhere in the text — the load-bearing format signal.
28
+ const INSN_SIGNAL = /\/\*\s*[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s*\*\//;
29
+ // A local-label DEFINITION on its own line (`.L800011C0_1DC0:`); the colon is required.
30
+ const LABEL_DEF = /^(\.[\w.$]+):$/;
31
+ // A GOT/PIC relocation operand this reader does not support (small-data / position-independent
32
+ // access) — declined loud. `%hi`/`%lo` are NOT here: they name a global's address and are preserved
33
+ // verbatim for the MIPS frontend to fold into a `gaddr` (see normalizeOperand / frontend/mips.ts).
34
+ const RELOC_OP = /%(gp_rel|gprel|got|call16|call_hi|call_lo|higher|highest|neg|tprel|dtprel)\b/i;
35
+ // Data directives whose bytes could encode an effect: skipping one inside a function slice would
36
+ // silently delete it, so they decline (mirrors the Thumb frontend's in-code-data guard).
37
+ const DATA_DIRECTIVE =
38
+ /^\.(byte|half|hword|short|2byte|word|4byte|long|dword|8byte|quad|float|double|ascii|asciz|string|incbin|space|skip|fill|zero)\b/i;
39
+
40
+ /** Does this text look like Splat-dialect MIPS? Both signals (`glabel` markers and the
41
+ * three-word instruction-comment prefix) are unique to Splat — objdump and compiler `.s` carry
42
+ * neither — so a positive match is unambiguous. */
43
+ export function isSplatMips(asm: string): boolean {
44
+ return /^\s*glabel\s+\S+/m.test(asm) || INSN_SIGNAL.test(asm);
45
+ }
46
+
47
+ /** Parse Splat-dialect text into one function's `DisasmInstr[]`. When `glabel` markers are present
48
+ * the text is sliced to exactly `name` (an absent symbol declines LOUD — emitting some other
49
+ * function's body under the requested name is the silent miscompile the cardinal rule forbids);
50
+ * a marker-less fragment is parsed whole. Branch targets are resolved against the local-label
51
+ * map, so an unresolved `.L` target declines here rather than crashing deep in the frontend. */
52
+ export function parseSplatMips(asm: string, name: string): DisasmInstr[] {
53
+ const lines = asm.split('\n');
54
+
55
+ // Slice to the requested function: `glabel NAME` … its `endlabel`/the next `glabel`/EOF.
56
+ const glabels: { line: number; sym: string }[] = [];
57
+ for (let i = 0; i < lines.length; i++) {
58
+ const m = lines[i].match(/^\s*glabel\s+(\S+)/);
59
+ if (m) {
60
+ glabels.push({ line: i, sym: m[1] });
61
+ }
62
+ }
63
+ let slice = lines;
64
+ if (glabels.length > 0) {
65
+ const at = glabels.findIndex((g) => g.sym === name);
66
+ if (at === -1) {
67
+ throw new FrontendUnsupportedError(
68
+ `symbol '${name}' not found in the Splat disassembly (functions present: ${glabels.map((g) => g.sym).join(', ')})`,
69
+ );
70
+ }
71
+ let end = lines.length;
72
+ for (let i = glabels[at].line + 1; i < lines.length; i++) {
73
+ if (/^\s*(endlabel|glabel)\b/.test(lines[i])) {
74
+ end = i;
75
+ break;
76
+ }
77
+ }
78
+ slice = lines.slice(glabels[at].line, end);
79
+ }
80
+
81
+ // Flatten to instructions, assigning any pending label(s) to the NEXT instruction's address.
82
+ const instrs: DisasmInstr[] = [];
83
+ const labelAddr = new Map<string, number>();
84
+ let pending: string[] = [];
85
+ for (const raw of slice) {
86
+ const line = raw.trim();
87
+ if (!line || line.startsWith('#')) {
88
+ continue;
89
+ }
90
+ if (/^(glabel|endlabel|dlabel|jlabel)\b/.test(line) || /^nonmatching\b/.test(line)) {
91
+ continue; // function/data markers and the objdiff scratch header
92
+ }
93
+ const labelDef = line.match(LABEL_DEF);
94
+ if (labelDef) {
95
+ pending.push(labelDef[1]);
96
+ continue;
97
+ }
98
+ if (line.startsWith('.')) {
99
+ // A data directive in the code stream could hide an instruction/effect — decline; other
100
+ // bookkeeping directives (`.set`, `.align`, `.section`…) are transparent and skipped.
101
+ if (DATA_DIRECTIVE.test(line)) {
102
+ throw new FrontendUnsupportedError(
103
+ `cannot lift '${name}': data directive '${line}' in the code stream — skipping it would silently delete its effect`,
104
+ );
105
+ }
106
+ continue;
107
+ }
108
+ const m = line.match(INSN_LINE);
109
+ if (!m) {
110
+ throw new FrontendUnsupportedError(
111
+ `cannot lift '${name}': unrecognised line in the Splat disassembly: '${line}'`,
112
+ );
113
+ }
114
+ const addr = parseInt(m[1], 16);
115
+ const mnemonic = m[2];
116
+ // A data directive carrying an instruction-comment prefix (`/* … */ .word …`) would otherwise
117
+ // be decoded as a mnemonic and silently become an opaque — decline it like the bare form.
118
+ if (DATA_DIRECTIVE.test(mnemonic)) {
119
+ throw new FrontendUnsupportedError(
120
+ `cannot lift '${name}': data directive '${mnemonic}' in the code stream — skipping it would silently delete its effect`,
121
+ );
122
+ }
123
+ const ops = m[3].trim() ? splitOperands(m[3].trim()).map((o) => normalizeOperand(name, o)) : [];
124
+ // addi/addiu SIGN-EXTEND their 16-bit immediate; Splat may spell the low half of a materialised
125
+ // constant as an unsigned mask (`(0x8000ABCD & 0xFFFF)` = 0xABCD), so re-sign it here to match
126
+ // the hardware — and the objdump path, which prints the already-signed value. Zero-extending ops
127
+ // (ori/andi/xori) and lui keep the unsigned value, so they are deliberately excluded.
128
+ if ((mnemonic === 'addiu' || mnemonic === 'addi') && ops.length === 3 && /^-?(0x[0-9a-fA-F]+|\d+)$/.test(ops[2])) {
129
+ ops[2] = String(signExtend16(ops[2]));
130
+ }
131
+ for (const l of pending) {
132
+ labelAddr.set(l, addr);
133
+ }
134
+ pending = [];
135
+ instrs.push({ addr, mnemonic, ops });
136
+ }
137
+
138
+ // Resolve every branch/jump's target label to an address. A target that is not a local label of
139
+ // this function — an unresolvable `.L`, or a bare symbol (`j func` tail call) — declines LOUD
140
+ // rather than leaving `target` undefined for the frontend to crash on (`succ(undefined)`).
141
+ for (const ins of instrs) {
142
+ if (!isBranchMnemonic(ins.mnemonic)) {
143
+ continue;
144
+ }
145
+ const label = ins.ops[ins.ops.length - 1];
146
+ const t = label !== undefined ? labelAddr.get(label) : undefined;
147
+ if (t === undefined) {
148
+ throw new FrontendUnsupportedError(
149
+ `cannot lift '${name}': branch/jump target '${label ?? ''}' is not a local label in this function ` +
150
+ `(tail call / cross-function branch not modelled)`,
151
+ );
152
+ }
153
+ ins.target = t;
154
+ }
155
+ return instrs;
156
+ }
157
+
158
+ // A control transfer whose last operand is a code-label target: `b`, `j`, and the conditional
159
+ // branches (`beq`/`bnez`/`bc1f`…). NOT `jal`/`jalr` (calls) or `jr` (register) — the MIPS frontend
160
+ // owns those declines; `break` is a trap, not a branch.
161
+ const isBranchMnemonic = (mn: string): boolean => mn === 'j' || (mn[0] === 'b' && mn !== 'break');
162
+
163
+ // Re-sign a raw 16-bit immediate: a value with bit 15 set becomes negative (two's complement),
164
+ // matching how addi/addiu sign-extend the field. A value already ≤ 0x7FFF is unchanged.
165
+ function signExtend16(s: string): number {
166
+ const v = parseInt(s, /^-?0x/i.test(s) ? 16 : 10) & 0xffff;
167
+ return v & 0x8000 ? v - 0x10000 : v;
168
+ }
169
+
170
+ // Split an operand list on top-level commas (commas inside `(...)` — a memory operand or a
171
+ // constant expression — do not separate operands).
172
+ function splitOperands(s: string): string[] {
173
+ const out: string[] = [];
174
+ let depth = 0;
175
+ let cur = '';
176
+ for (const ch of s) {
177
+ if (ch === '(') {
178
+ depth++;
179
+ } else if (ch === ')') {
180
+ depth--;
181
+ }
182
+ if (ch === ',' && depth === 0) {
183
+ out.push(cur.trim());
184
+ cur = '';
185
+ continue;
186
+ }
187
+ cur += ch;
188
+ }
189
+ if (cur.trim()) {
190
+ out.push(cur.trim());
191
+ }
192
+ return out;
193
+ }
194
+
195
+ // Rewrite one Splat operand into the canonical objdump spelling the frontend consumes: strip the
196
+ // `$` register sigil, fold a memory operand's displacement expression, evaluate a bare constant
197
+ // expression, preserve a `%hi`/`%lo` global reference, and decline an unsupported PIC relocation.
198
+ function normalizeOperand(name: string, op: string): string {
199
+ // `%hi(SYM)` / `%lo(SYM + N)` / `%lo(SYM)(base)` — a global's address. Preserved verbatim (with a
200
+ // de-sigiled base) for the MIPS frontend to fold into a `gaddr`; NOT declined like the PIC relocs.
201
+ const hilo = op.match(/^(%(?:hi|lo)\([^)]*\))(?:\((\$?[A-Za-z]\w*)\))?$/);
202
+ if (hilo) {
203
+ return hilo[2] ? `${hilo[1]}(${hilo[2].replace(/^\$/, '')})` : hilo[1];
204
+ }
205
+ if (RELOC_OP.test(op)) {
206
+ throw new FrontendUnsupportedError(
207
+ `cannot lift '${name}': relocation operand '${op}' (small-data / PIC data access) — not modelled`,
208
+ );
209
+ }
210
+ // Memory operand `DISP(base)` — base is a register (letter-first), DISP a constant/expression.
211
+ const mem = op.match(/^(.*)\((\$?[A-Za-z]\w*)\)$/);
212
+ if (mem) {
213
+ const disp = mem[1].trim();
214
+ const off = disp === '' ? '0' : String(evalConst(name, disp));
215
+ return `${off}(${mem[2].replace(/^\$/, '')})`;
216
+ }
217
+ // A bare constant expression (`(0x660104 >> 16)`) — the assembler's hi/lo literal split.
218
+ if (op.startsWith('(')) {
219
+ return String(evalConst(name, op));
220
+ }
221
+ return op.replace(/^\$/, '');
222
+ }
223
+
224
+ // Evaluate a constant integer expression (the assembler's hi/lo split: hex/dec literals with
225
+ // `+ - * << >> & | ^ ~` and parentheses). Precedence-climbing; C-like precedence. A shift `>>` is
226
+ // LOGICAL — Splat's operands are unsigned 32-bit constants. Anything unparsable declines LOUD
227
+ // rather than silently yielding NaN.
228
+ function evalConst(name: string, expr: string): number {
229
+ const toks = expr.match(/0x[0-9a-fA-F]+|\d+|<<|>>|[-+*&|^()~]/g);
230
+ if (!toks) {
231
+ throw new FrontendUnsupportedError(`cannot lift '${name}': unparsable constant expression '${expr}'`);
232
+ }
233
+ const prec: Record<string, number> = { '|': 1, '^': 2, '&': 3, '<<': 4, '>>': 4, '+': 5, '-': 5, '*': 6 };
234
+ let p = 0;
235
+ const fail = () => {
236
+ throw new FrontendUnsupportedError(`cannot lift '${name}': unparsable constant expression '${expr}'`);
237
+ };
238
+ const primary = (): number => {
239
+ const t = toks[p++];
240
+ if (t === undefined) {
241
+ return fail();
242
+ }
243
+ if (t === '(') {
244
+ const v = expr2(0);
245
+ if (toks[p++] !== ')') {
246
+ return fail();
247
+ }
248
+ return v;
249
+ }
250
+ if (t === '-') {
251
+ return -unary();
252
+ }
253
+ if (t === '~') {
254
+ return ~unary();
255
+ }
256
+ if (/^(0x[0-9a-fA-F]+|\d+)$/.test(t)) {
257
+ return t.toLowerCase().startsWith('0x') ? parseInt(t, 16) : parseInt(t, 10);
258
+ }
259
+ return fail();
260
+ };
261
+ const unary = (): number => primary();
262
+ const expr2 = (minPrec: number): number => {
263
+ let left = unary();
264
+ for (;;) {
265
+ const op = toks[p];
266
+ if (op === undefined || prec[op] === undefined || prec[op] < minPrec) {
267
+ break;
268
+ }
269
+ p++;
270
+ const right = expr2(prec[op] + 1);
271
+ switch (op) {
272
+ case '+':
273
+ left = (left + right) | 0;
274
+ break;
275
+ case '-':
276
+ left = (left - right) | 0;
277
+ break;
278
+ case '*':
279
+ left = Math.imul(left, right);
280
+ break;
281
+ case '<<':
282
+ left = (left << right) >>> 0;
283
+ break;
284
+ case '>>':
285
+ left = left >>> right;
286
+ break;
287
+ case '&':
288
+ left = left & right;
289
+ break;
290
+ case '|':
291
+ left = left | right;
292
+ break;
293
+ case '^':
294
+ left = left ^ right;
295
+ break;
296
+ }
297
+ }
298
+ return left;
299
+ };
300
+ const v = expr2(0);
301
+ if (p !== toks.length) {
302
+ return fail();
303
+ }
304
+ return v;
305
+ }
@@ -1,4 +1,4 @@
1
- // asmlift — magic-number constant-division recovery (L1 recognition; gcc-mips + mwcc-ppc).
1
+ // asmlift — magic-number constant-division recovery (L1 recognition; gcc2.7.2kmc + mwcc_242_81).
2
2
  //
3
3
  // A compiler replaces `x / C` for a non-power-of-2 constant `C` with a HIGH-WORD MULTIPLY by a
4
4
  // precomputed "magic" reciprocal `M`, a shift `s`, and a sign correction. The frontend lifts the