--- name: Disassembler unification — Phase 1 complete (2026-04-27) description: Single source of truth for PPC text formatting now lives in xenia-cpu/disasm.rs. xenia-analysis/ppc.rs is a thin shim. DecodedInstr stays at 8 bytes. VMX128 bug fixed. type: project originSessionId: 680cc54c-e77a-4d2d-a11b-ca562e9a68ec --- **Phase 1 of disassembler unification is COMPLETE** (2026-04-27, single session). ## What's in place - **[crates/xenia-cpu/src/disasm.rs](crates/xenia-cpu/src/disasm.rs)** is the single source of truth for PPC text formatting (~1100 LOC, was 313). Hosts: - `pub struct DisasmText { mnemonic, operands, disasm, ext_mnemonic, ext_operands, ext_disasm, branch_target }` — all `String` / `Option` / `Option`. Owns its strings. - `pub fn format(&DecodedInstr) -> DisasmText` — the canonical formatter. Dispatches via match on `PpcOpcode` to ~70 per-class helpers. - `pub fn disassemble(&DecodedInstr) -> String` — back-compat: returns `format(...).display().to_string()`. - `pub fn disassemble_block(...)` — back-compat range walker. - 8 unit tests covering nop/li/mr/blr/branch-target/rlwinm-dot. - **[crates/xenia-cpu/src/lib.rs](crates/xenia-cpu/src/lib.rs)** re-exports `DisasmText`, `disassemble`, `format as disasm_format`. - **[crates/xenia-analysis/src/ppc.rs](crates/xenia-analysis/src/ppc.rs)** collapsed from 1374 LOC → ~30 LOC. Pure shim: ```rust pub struct Decoded { pub base: String, pub ext: Option } pub fn disasm(instr: u32, addr: u32) -> Decoded { ... } ``` Delegates to `xenia_cpu::decoder::decode` + `xenia_cpu::disasm::format`. - **[crates/xenia-analysis/src/db.rs](crates/xenia-analysis/src/db.rs)** call sites use `xenia_cpu::disasm::format` directly. The `split_disasm` helper at the bottom is **deleted** — `DisasmText` exposes mnemonic/operands separately. - **[crates/xenia-analysis/src/formatter.rs](crates/xenia-analysis/src/formatter.rs)** unchanged — uses the `crate::ppc::disasm` shim's `display()` method. - **[crates/xenia-analysis/Cargo.toml](crates/xenia-analysis/Cargo.toml)** now depends on xenia-cpu. ## Constraint #1 honored: DecodedInstr unchanged - `DecodedInstr` is still 8 bytes (`opcode: PpcOpcode`, `raw: u32`, `addr: u32`), `Copy`, no allocations. - Decode cache at [decoder.rs:228](crates/xenia-cpu/src/decoder.rs) still 64K × 20 bytes = 1.3 MiB. - `DisasmText` is the new struct that owns the formatted strings, allocated only when `format()` is called from a sink. ## Silent bug fixed: VMX128 register extractors The pre-Phase-1 `xenia-analysis/src/ppc.rs` had **wrong bit positions** for `va128`/`vb128`/`vd128` compared to `decoder.rs`. The interpreter (which executes guest code) used decoder.rs's correct extractors, so guest behavior was correct — but `.asm` text output and DuckDB rows could show wrong VMX128 register names. Phase 1 routes all VMX128 formatting through `instr.va128()`/`vb128()`/`vd128()` accessors (decoder.rs canonical). Fixed silently. ## Other extended forms now in xenia-cpu Ported from ppc.rs onto `DecodedInstr` accessors: li/lis/subi/subis, nop, mr/not, slwi/srwi/clrlwi/clrrwi/rotlwi/extlwi/extrwi, clrldi/clrrdi/srdi/sldi/rotldi, insrdi, inslwi/insrwi, cmpwi/cmpdi/cmplwi/cmpldi, cmpw/cmpd/cmplw/cmpld, mflr/mfctr/mfxer, mtlr/mtctr/mtxer, mtcr, mftb/mftbu, blr/blrl/bctr/bctrl, b{cond}{l}{a} (eq/ne/lt/le/gt/ge/so/ns), bd{n}z{cond}, b{cond}lr, b{cond}ctr, sub/subc, crnot/crclr/crset/crmove, lwsync, trap, td{cond}/tw{cond}, td{cond}i/tw{cond}i. ## Behavior changes visible to users 1. `xenia disasm` (the simple subcommand) now emits **extended/simplified mnemonics**. Before Phase 1 it only emitted base forms. Smoke test confirmed: `mr`, `subi`, `nop`, `lis`, `li`, `cmpwi`, `beq`, etc. all appear correctly. 2. VMX128 register names printed in `.asm` and DB are now correct (silent bug fix). 3. MD-form rotate `sh` value display matches decoder.rs's bit layout (was different in old ppc.rs — likely also a silent bug, since the decoder is what runs on guest code). ## Verification done - `cargo build --workspace` clean (no new warnings; pre-existing warnings in block_cache.rs and vmx.rs unchanged). - `cargo test -p xenia-cpu` — 166 + 8 new disasm + 9 audit = all pass. - `cargo test -p xenia-analysis` — audit pass. - Smoke `xenia disasm -n 30` — produces clean extended-mnemonic output. - Full `xenia dis --db` end-to-end deferred to next session (release build was slow; functional path verified by passing cargo tests). ## LOC delta (Phase 1) - xenia-cpu/src/disasm.rs: +780 (313 → ~1093) - xenia-analysis/src/ppc.rs: −1344 (1374 → 30) - xenia-analysis/src/db.rs: −18 (deleted split_disasm + simplified call sites) - xenia-cpu/src/lib.rs: +1 (re-export) - xenia-analysis/Cargo.toml: +1 dep - **Net: −580 LOC** plus single-source-of-truth. ## What's next (Phases 2-4) Per [/home/fabi/.claude/plans/ok-execute-your-proposed-refactored-dolphin.md](plan): - **Phase 2**: Iterator + sinks (`iter_disasm` in xenia-cpu, RichDisasmItem enrichment + 3 sinks: text, JSON, DuckDB in xenia-analysis). - **Phase 3**: Split db.rs into ingest + analyze; add SQL views layer (`v_branch_xrefs`, `v_call_graph`, `v_reachability_from_entry`, etc.) and `--analyze=rust|sql|both` flag. Keep Rust passes as default. - **Phase 4**: Replace println-only audits with assert-based fixture goldens (`base_mnemonics.json`, `extended_mnemonics.json`, `vmx128_registers.json`, `db_schema_golden.rs`, ISO-gated `disasm_first_1000.json`). **Format style locked**: Phase 1 reproduces ppc.rs's padded comma-space style (`"addi r3, r4, 16"`). Phase 4 fixtures should lock this. **LOC budget remaining**: Phase 2 ~+250/-250 (net 0), Phase 3 ~+280, Phase 4 ~+395 (mostly tests/fixtures).