""" Scrapes Xenia Canary's PPC front-end for each instruction's semantic implementation, `InstrEmit_`, at a PINNED commit. The files are: src/xenia/cpu/ppc/ppc_emit_alu.cc (integer ALU) src/xenia/cpu/ppc/ppc_emit_memory.cc (loads/stores/cache/sync) src/xenia/cpu/ppc/ppc_emit_altivec.cc (VMX + VMX128) src/xenia/cpu/ppc/ppc_emit_fpu.cc (floating-point) src/xenia/cpu/ppc/ppc_emit_control.cc (branch/CR/SPR/syscall/trap) Returns, for each mnemonic, the file, the starting line of the `int InstrEmit_(...)` definition, and the full function text. 🔴 Everything is read through `git show :`, never from the checkout's working tree. The local Canary checkout is a working branch: it carries our instrumentation, and it can sit months behind upstream — at the time this was written it lacked upstream's fix to `mcrf` ("copy the CR field instead of comparing it against zero"). A manual quoting the working tree would publish probes and a wrong `mcrf` as "Canary semantics". Pinning also makes every line number in the manual match the commit its links name. 🔴 And a missing input is an ERROR, not an empty result. The previous version skipped any file it could not find, and when the manual moved into `tools/ppc-manual/` every source path stopped resolving without a single warning. """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path import re import subprocess CXX_EMIT_FILES = [ "src/xenia/cpu/ppc/ppc_emit_alu.cc", "src/xenia/cpu/ppc/ppc_emit_memory.cc", "src/xenia/cpu/ppc/ppc_emit_altivec.cc", "src/xenia/cpu/ppc/ppc_emit_fpu.cc", "src/xenia/cpu/ppc/ppc_emit_control.cc", ] XML_PATH_IN_CANARY = "tools/ppc-instructions.xml" class CanarySource: """One Canary commit, read through git.""" def __init__(self, repo: Path, ref: str): self.repo = repo if not (repo / ".git").exists(): raise SystemExit(f"no Canary git checkout at {repo} (pass --canary)") res = subprocess.run( ["git", "-C", str(repo), "rev-parse", "--verify", f"{ref}^{{commit}}"], capture_output=True, text=True, ) if res.returncode != 0: raise SystemExit( f"Canary ref {ref!r} does not resolve in {repo} — fetch it first " f"(`git -C {repo} fetch origin`) or pass --canary-ref" ) self.ref = ref self.sha = res.stdout.strip() def read(self, rel: str) -> str: res = subprocess.run( ["git", "-C", str(self.repo), "show", f"{self.sha}:{rel}"], capture_output=True, text=True, ) if res.returncode != 0: raise SystemExit(f"{rel} is not in Canary @ {self.sha[:10]}: {res.stderr.strip()}") return res.stdout @dataclass class CxxRef: mnem: str emit_file: str | None = None # relative to the Canary repository root emit_line: int | None = None emit_body: str = "" # the whole `int InstrEmit_…(…) { … }` text def _cxx_ident(mnem: str) -> str: """Canary maps '.' in the mnemonic to a trailing 'x' in the C++ symbol (e.g. addic. → InstrEmit_addicx).""" return mnem.replace(".", "x") def _live_lines(lines: list[str]) -> list[bool]: """Per line: is it compiled? Models just enough of the preprocessor to count braces honestly. `#if 0` blocks are dead; for any other conditional only the FIRST branch is taken, so `#if X { … #else { … #endif` counts one opening brace rather than two. `InstrEmit_branch` straddles exactly such an `#if 0 / #else / #endif`, and a naive counter ran past its end to the close of the namespace. """ live: list[bool] = [] stack: list[list[bool]] = [] # per level: [a branch was already taken, this branch is live] for raw in lines: t = raw.strip() if t.startswith("#if"): taken = not re.match(r"#if\s+0\b", t) stack.append([taken, taken]) live.append(False) elif t.startswith("#el"): # #else / #elif: live only if nothing was taken yet if stack: stack[-1][1] = not stack[-1][0] stack[-1][0] = True live.append(False) elif t.startswith("#endif"): if stack: stack.pop() live.append(False) else: live.append(all(level[1] for level in stack)) return live def _function_end(lines: list[str], start: int, live: list[bool]) -> int: """Index of the line holding the brace that closes the function opened at `start`. Ignores braces in `//` comments, string/char literals and preprocessor-dead lines. """ depth = 0 opened = False for j in range(start, len(lines)): if not live[j]: continue code = re.sub(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'', "", lines[j]) code = code.split("//", 1)[0] for ch in code: if ch == "{": depth += 1 opened = True elif ch == "}": depth -= 1 if opened and depth == 0: return j raise ValueError(f"unterminated function starting at line {start + 1}") _DELEGATION = re.compile(r"^\s*return\s+InstrEmit_([A-Za-z0-9_]+)\s*\(") class CxxScraper: def __init__(self, canary: CanarySource): self.canary = canary self._index: dict[str, tuple[str, int, str]] = {} fn_pat = re.compile(r"^\s*int\s+InstrEmit_([A-Za-z_][A-Za-z0-9_]*)\s*\(") for rel in CXX_EMIT_FILES: lines = canary.read(rel).splitlines() live = _live_lines(lines) for i, line in enumerate(lines): m = fn_pat.match(line) if not m: continue end = _function_end(lines, i, live) body = "\n".join(lines[i:end + 1]) self._index.setdefault(m.group(1), (rel, i + 1, body)) if not self._index: raise SystemExit(f"no InstrEmit_ functions found in Canary @ {canary.sha[:10]}") def __len__(self) -> int: return len(self._index) def delegate_of(self, name: str) -> str | None: """The helper a PURE one-line delegation calls, e.g. `InstrEmit_stvx` is only `return InstrEmit_stvx_(f, i, …);`. 134 of 522 emitters are like this, and a snapshot of the wrapper alone shows no semantics.""" body = self._index.get(name, ("", 0, ""))[2].splitlines() inner = [l for l in body[1:-1] if l.strip() and not l.strip().startswith("//")] if len(inner) == 1: m = _DELEGATION.match(inner[0]) if m and m.group(1) in self._index and m.group(1) != name: return m.group(1) return None def lookup(self, mnem: str) -> CxxRef: name = _cxx_ident(mnem) hit = self._index.get(name) if hit is None: return CxxRef(mnem=mnem) body = hit[2] seen = {name} helper = self.delegate_of(name) while helper and helper not in seen: # follow the chain, never loop seen.add(helper) h = self._index[helper] body += f"\n\n// ── delegates to ({h[0]}:{h[1]}) ──\n" + h[2] helper = self.delegate_of(helper) return CxxRef(mnem=mnem, emit_file=hit[0], emit_line=hit[1], emit_body=body) if __name__ == "__main__": import sys repo = Path(sys.argv[1]) if len(sys.argv) > 1 else \ Path(__file__).resolve().parents[3].parent / "xenia-canary" s = CxxScraper(CanarySource(repo, "origin/canary_experimental")) print(f"{len(s)} emitters @ {s.canary.sha[:10]}") for m in ("addcx", "addic.", "lwz", "bclrx", "mfspr", "stvx", "vaddfp", "vaddfp128", "faddx", "mcrf"): r = s.lookup(m) print(f"{m:12s} {r.emit_file}:{r.emit_line} ({r.emit_body.count(chr(10)) + 1 if r.emit_body else 0} lines)")