Files
melee/tools/replace-includes.py
Robin AveryandClaude Opus 5.5 02c58abe32 Implement initial dat file annotations and tooling (#3587)
See tools/dat-cli/README.md.

The macros in dat_macros.h write annotations into the DWARF built by the
ppc-dwarf CMake preset (melee-dwarf in nix). The tool is AI-assisted and
incomplete; tools/dat-cli/TODO.md lists what's missing. It will
eventually hook into objdiff and generate its own coverage/progress
metrics.

The annotations are parsed by tools/dat-cli/src/interop/dwarf/
annotation.rs and expr.rs. The expressions are a bit complex because
they need to describe conditionals that reference struct fields, but
wouldn't be that hard to implement for downstream tooling in other
languages. This makes the DWARF valuable to ports and other use cases
for reading dat files.

In the DWARF build, the archive loaders record which type each symbol
is loaded as: the lbArchive loaders through DAT_ROOTS, and
HSD_ArchiveGetPublicAs, a typed HSD_ArchiveGetPublicAddress that
replaces most call sites. Checking those types against the archives
turned up wrong ones, which are fixed here: DynamicsTemplate is split
from DynamicsDesc, UnkStageDat's shadow materials, HSD_AObjDesc.obj_id,
the particle banks, the item throw table, and a number of void*
destinations. placeholder.h and m2c_macros.h move to
libs/doldecomp/include alongside dat_macros.h.

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-29 22:52:07 -04:00

163 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import logging
import re
from pathlib import Path
IN_ROOTS = [
Path.cwd().joinpath(s).resolve(strict=True)
for s in [
"src",
"src/Runtime",
"src/MSL",
"src/melee",
"src/melee/ft/kinds",
"libs/doldecomp/include",
"libs/dolphin/include",
"libs/dolphin/src",
"build/GALE01/include",
]
]
OUT_ROOTS = [
IN_ROOTS[2], # src/MSL
IN_ROOTS[0], # src
IN_ROOTS[5], # libs/dolphin/include
IN_ROOTS[7], # build/GALE01/include
]
INCLUDE_RE = re.compile(
r'^#include [<"](?P<path>.*?)[>"](?P<remaining>.*)$', re.MULTILINE
)
DEFAULT_FMT = "#include <{}>"
def parse_args() -> list[Path]:
parser = argparse.ArgumentParser(
description="Rewrite #include paths in selected C/H files."
)
parser.add_argument(
"paths",
nargs="+",
type=Path,
help="One or more existing .c/.h files under an IN_ROOT.",
)
args = parser.parse_args()
files: list[Path] = []
for raw_path in args.paths:
try:
path = raw_path.resolve(strict=True)
except FileNotFoundError:
parser.error(f"path does not exist: {raw_path}")
if not any(path.is_relative_to(root) for root in IN_ROOTS):
parser.error(f"path is not under any IN_ROOT: {raw_path}")
if not path.is_file() or not path.match("*.[ch]"):
parser.error(f"path does not match *.[ch]: {raw_path}")
files.append(path)
# Avoid processing the same file twice if it was supplied more than once.
return list(dict.fromkeys(files))
def rewrite_file(src_path: Path) -> None:
logging.debug("Opening `%s`", src_path)
text = src_path.read_text(encoding="utf-8", errors="strict", newline="")
local_root = src_path.parent.resolve(strict=True)
in_roots = [local_root, *IN_ROOTS]
out_roots = [*OUT_ROOTS]
def repl(m: re.Match[str]) -> str:
mp = Path(m["path"])
s = m[0]
logging.debug("Evaluating `%s`", s)
def put(p: Path) -> str:
p = p.resolve()
def decide_fmt() -> tuple[Path, str]:
if p.suffix not in {".c", ".h", ".inc"}:
logging.warning("Deleting suspicious include `%s`", m[0])
return p, ""
if src_path.suffix == ".c":
try:
return p.relative_to(src_path.parent), '#include "{}"{}'
except ValueError:
pass
for root in out_roots:
try:
return p.relative_to(root), "#include <{}>{}"
except ValueError:
continue
logging.error("Out root not found: %s", mp)
return p, m[0]
p, fmt = decide_fmt()
s = fmt.format(p.as_posix(), m["remaining"])
logging.info("Replaced `%s`", s)
return s
def search_relative() -> str | None:
if src_path.suffix == ".c" and mp.is_relative_to(src_path.parent):
logging.info(
"Relative include: `%s`", p := mp.relative_to(src_path.parent)
)
return put(p)
return None
def search_sus() -> str | None:
if mp.suffix not in {".c", ".h", ".inc"}:
return put(mp)
return None
def search_roots() -> str | None:
for root in in_roots:
logging.debug("Trying `%s` / `%s`", root, str(mp))
try:
resolved = root.joinpath(mp).resolve(strict=True)
logging.info("Found `%s`", resolved)
return put(resolved)
except FileNotFoundError:
logging.debug("Did not find `%s` in `%s`", mp, root)
continue
return None
s = next(
filter(
lambda x: x is not None,
map(
lambda f: f(),
[
search_relative,
search_sus,
search_roots,
],
),
),
None,
)
if s is None:
logging.error("Didn't find `%s`", s := m[0])
return s
_ = src_path.write_text(
INCLUDE_RE.sub(repl, text), encoding="utf-8", errors="strict", newline=""
)
def main() -> None:
logging.basicConfig(level=logging.WARNING)
for src_path in parse_args():
rewrite_file(src_path)
if __name__ == "__main__":
main()