""" Locates each instruction in Sylpheed's own PowerPC decoder, `crates/sylpheed-ppc` — the decoder that produces the disassembly in `sylpheed.db`, and so the one whose mnemonics a reader of that database sees. Outputs produced for each mnemonic: - opcode_line: line in `crates/sylpheed-ppc/src/opcode.rs` where the `PpcOpcode` variant is declared (1-indexed) - decoder_line: line in `crates/sylpheed-ppc/src/decoder.rs` where the variant is produced from raw bits This used to scrape `xenia-rs/crates/xenia-cpu/src/`, including an interpreter-arm snapshot. `xenia-rs` is retired and archived, and the decoder half was lifted into `sylpheed-ppc` unchanged in shape — the same `pub enum PpcOpcode` and `PpcOpcode::` producers — so only the path and the interpreter half changed. Semantics now come from Canary; see `cxx_scraper.py`. 🔴 Missing sources are an error, not an empty index. The previous version returned `[]` for a file it could not find, so once the manual moved and the path stopped resolving, every page silently lost its references. """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path import re DECODER_CRATE = Path("crates") / "sylpheed-ppc" / "src" @dataclass class DecoderRef: mnem: str opcode_line: int | None = None decoder_line: int | None = None def _ident(mnem: str) -> str: """XML mnemonic -> `PpcOpcode` variant: `.` is not legal in a Rust identifier, so `addic.` is `addicx` (the same rule Canary uses).""" return mnem.replace(".", "x") class DecoderScraper: def __init__(self, repo_root: Path): self.src = repo_root / DECODER_CRATE self._opcode_lines = self._read_lines(self.src / "opcode.rs") self._decoder_lines = self._read_lines(self.src / "decoder.rs") self._opcode_index = self._index_opcode_enum() self._decoder_index = self._index_decoder() if not self._opcode_index or not self._decoder_index: raise SystemExit(f"no PpcOpcode variants/producers found under {self.src}") @staticmethod def _read_lines(path: Path) -> list[str]: if not path.is_file(): raise SystemExit(f"decoder source missing: {path}") return path.read_text(encoding="utf-8").splitlines() def _index_opcode_enum(self) -> dict[str, int]: """Map identifier -> 1-indexed line inside `pub enum PpcOpcode { ... }` (several identifiers may share a line).""" idx: dict[str, int] = {} token = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b") in_enum = False for i, line in enumerate(self._opcode_lines, start=1): if "pub enum PpcOpcode" in line: in_enum = True continue if not in_enum: continue if line.startswith("}"): break code = line.strip().split("//", 1)[0] for m in token.finditer(code): idx.setdefault(m.group(1), i) return idx def _index_decoder(self) -> dict[str, int]: """Map identifier -> 1-indexed line of its FIRST `PpcOpcode::` occurrence, i.e. where the decoder produces it.""" idx: dict[str, int] = {} pat = re.compile(r"PpcOpcode::([A-Za-z_][A-Za-z0-9_]*)") for i, line in enumerate(self._decoder_lines, start=1): for m in pat.finditer(line): idx.setdefault(m.group(1), i) return idx def lookup(self, mnem: str) -> DecoderRef: ident = _ident(mnem) return DecoderRef(mnem=mnem, opcode_line=self._opcode_index.get(ident), decoder_line=self._decoder_index.get(ident)) if __name__ == "__main__": root = Path(__file__).resolve().parents[3] s = DecoderScraper(root) for m in ("addcx", "addic.", "lwz", "bclrx", "mfspr", "stvx", "vaddfp", "vaddfp128", "faddx", "mcrf"): r = s.lookup(m) print(f"{m:12s} opcode@{r.opcode_line} decoder@{r.decoder_line}")