From 90a82da283e26baa96bdd99f3dea8967f7537006 Mon Sep 17 00:00:00 2001 From: Leandro Soares Date: Sun, 11 Oct 2026 11:44:28 +0000 Subject: [PATCH] feat(tools): mark behaviourally empty bodies in the progress report Cherry-picked from commit f1be92e95c02be451971fc1a8eec139e738e936d. --- docs/dev/PROGRESS.md | 2 + tools/progress.py | 67 +++++++++++++++++++++++++++++++-- tools/tests/test_progress.py | 73 ++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 9daf9fc04..dec716d9f 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -18,6 +18,8 @@ Comparisons identify functions by library and name. When a head group becomes a Real removals and transitions back to stubs remain visible. Percentages retain the original snapshot totals, so eliminating duplicate accounting can change a percentage without changing available functionality. Older JSON snapshots without `shared_sources` are accepted. +Implemented definitions are also classified as behaviourally empty when their body holds only `(void)name;` discards and at most one constant return, bare `return;` or `throw`, including a return of a local `static` wrapper that is itself behaviourally empty; wrapper calls whose arguments contain calls do not qualify. Comparisons list newly implemented behaviourally empty functions without changing any count. Older JSON snapshots without `inert_names` are accepted. + ## Verification ```sh diff --git a/tools/progress.py b/tools/progress.py index c052dab1b..55f770dee 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -17,6 +17,11 @@ CMAKE_SHARED = re.compile(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\ SOURCE_SHARED = re.compile(r'^\s*#\s*include\s*"prx/([\w.-]+)/[\w./-]+\.cpp"', re.M) STUB_WRAPPER = re.compile(r"\bstatic\s+(?:\[\[noreturn\]\]\s+)?[\w:<>,\s*&]+?\s+(\w+)\s*\([^;{]*\)\s*\{") NON_CODE = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'|//[^\n]*|/\*.*?\*/', re.S) +DISCARD = re.compile(r"\(void\)\s*\w+\s*;") +EMPTY_RETURN = re.compile(r"return\s*;") +CONST_RETURN = re.compile(r"return\s+(?:[+-]?\s*(?:0[xX][0-9a-fA-F]+|\d+)[uUlL]*|\w+|\"(?:\\.|[^\"\\])*\")\s*;") +WRAPPER_RETURN = re.compile(r"return\s+(\w+)\s*\(([^()]*)\)\s*;") +BARE_THROW = re.compile(r"throw\b[^;]*;") FLAT_SEGMENTS = ("GLOBAL_", "SCRATCH_") OPCODE_SENTINELS = {"Invalid", "Count", "Unknown", "Unsupported"} OPCODE_ALIASES = { @@ -91,22 +96,73 @@ def stub_calls(text): return calls +def statements(body): + depth, start, parts = 0, 1, [] + for i, character in enumerate(body[1:], 1): + if character == "{": + depth += 1 + elif character == "}": + depth -= 1 + elif character == ";" and not depth: + parts.append(body[start:i + 1]) + start = i + 1 + parts.append(body[start:]) + return parts + + +def inert_statement(statement, wrappers): + if EMPTY_RETURN.fullmatch(statement) or CONST_RETURN.fullmatch(statement) or BARE_THROW.fullmatch(statement): + return True + call = WRAPPER_RETURN.fullmatch(statement) + return bool(call) and call.group(1) in wrappers + + +def inert_body(body, wrappers): + terminals = 0 + for statement in statements(body): + statement = statement.strip() + if not statement: + continue + if DISCARD.fullmatch(statement): + continue + if terminals or not inert_statement(statement, wrappers): + return False + terminals += 1 + return True + + +def inert_wrappers(text): + bodies = {m.group(1): text[m.end() - 1:body_end(text, m.end() - 1)] for m in STUB_WRAPPER.finditer(text)} + names = set() + while True: + found = {name for name, body in bodies.items() if name not in names and inert_body(body, names)} + if not found: + return names + names |= found + + def scan_library(path): - done, todo = set(), set() + done, todo, inert, live = set(), set(), set(), set() for source in path.rglob("*.cpp"): if "tests" in source.relative_to(path).parts: continue text = code(source.read_text(errors="ignore")) calls = stub_calls(text) + wrappers = inert_wrappers(text) for match in DEFINITION.finditer(text): name = match.group(1) if name.endswith("_nid_no_patch"): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] - (todo if any(call in body for call in calls) else done).add(name) + if any(call in body for call in calls): + todo.add(name) + continue + done.add(name) + (inert if inert_body(body, wrappers) else live).add(name) todo -= done + inert -= live group = {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo), - "done_names": sorted(done), "todo_names": sorted(todo)} + "done_names": sorted(done), "todo_names": sorted(todo), "inert_names": sorted(inert)} if not (done or todo): cmake = path / "CMakeLists.txt" texts = [(CMAKE_SHARED, cmake.read_text())] if cmake.is_file() else [] @@ -293,7 +349,8 @@ def summary(libraries, shaders): f'

Generated by tools/progress.py on every push to main. ' 'Raw numbers: progress.json.

', table("System libraries", "Library", libraries), - "

A function is implemented when it no longer calls NotImplemented_nid_no_patch. " + "

A function is implemented when it no longer calls NotImplemented_nid_no_patch; " + "comparison reports separately mark implemented functions whose body only returns a constant or throws. " f'The total only includes functions already declared in core/libs/prx, ' "not every function exported by the PS5 firmware. Shared-source libraries are counted only at their source library. " "Libraries with no exports only provide an empty module for titles that load them. " @@ -329,6 +386,7 @@ def compare(title, column, unit, base, head): base_all, head_all = base_done | names(base, "todo", owners), head_done | names(head, "todo", owners) implemented, declared = head_done - base_done, head_all - base_all - head_done regressed, removed = base_done & (head_all - head_done), base_all - head_all + inert = implemented & names(head, "inert", owners) if not (implemented or declared or regressed or removed or shared): return [] delta = round(head["percent"] - base["percent"], 2) @@ -338,6 +396,7 @@ def compare(title, column, unit, base, head): changes = f', {", ".join(counts)} {unit}' if counts else "" lines = [f'{icon} **{title}**: {head["percent"]}% ({delta:+}%{changes})', ""] lines += details("✅", "implemented", column, implemented) + lines += details("⚪", "behaviourally empty (constant return or throw)", column, inert) lines += details("🆕", "declared as stubs", column, declared) lines += details("⚠️", "went back to stubs", column, regressed) lines += details("🗑️", "removed", column, removed) diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 15f88dd2c..4aadeb611 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -121,6 +121,79 @@ class ProgressTests(unittest.TestCase): self.assertIn("-1 removed", report) self.assertIn("Pending", report) + def test_inert_bodies_are_marked_without_changing_counts(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Support() { return 0; }\n" + "static int Compute(int x) { return x * 2; }\n" + "int APS5_VABI A() { return 0; }\n" + "int APS5_VABI B(const void* p) { (void)p; return SCE_KERNEL_ERROR_EOPNOTSUPP; }\n" + "int APS5_VABI C(void* p) { (void)p; return 0x8A810001; }\n" + 'int APS5_VABI D() { throw std::runtime_error("no"); }\n' + "int APS5_VABI E() { return Support(); }\n" + "int APS5_VABI F(int a, int b) { return a + b; }\n" + "int APS5_VABI G() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int APS5_VABI H(int a) { return Compute(a); }\n") + group = progress.scan_library(library) + self.assertEqual(group["inert_names"], ["A", "B", "C", "D", "E"]) + self.assertEqual(group["done_names"], ["A", "B", "C", "D", "E", "F", "H"]) + self.assertEqual(group["todo_names"], ["G"]) + + def test_compare_marks_behaviourally_empty_implementations(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + library = root / "libSceExample" + library.mkdir() + kept = ( + "int APS5_VABI Ready() { return 0; }\n" + "int APS5_VABI Stubbed() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "static int Compute(int x) { return x * 2; }\n" + "int APS5_VABI Wrapped(int a) { return Compute(a); }\n") + added = ( + "static int Support() { return 0; }\n" + "int APS5_VABI A() { return 0; }\n" + "int APS5_VABI B(const void* p) { (void)p; return SCE_KERNEL_ERROR_EOPNOTSUPP; }\n" + "int APS5_VABI C(void* p) { (void)p; return 0x8A810001; }\n" + 'int APS5_VABI D() { throw std::runtime_error("no"); }\n' + "int APS5_VABI E() { return Support(); }\n" + "int APS5_VABI F(int a, int b) { return a + b; }\n") + (library / "Export.cpp").write_text(kept) + with patch.object(progress, "PRX", root): + base = progress.collect_libraries() + for group in base["groups"]: + del group["inert_names"] + (library / "Export.cpp").write_text(kept + added) + with patch.object(progress, "PRX", root): + head = progress.collect_libraries() + report = "\n".join(progress.compare("Libraries", "Library", "functions", base, head)) + self.assertIn("+6 implemented", report) + self.assertIn("

⚪ 5 behaviourally empty (constant return or throw)", report) + empty = report.split("behaviourally empty (constant return or throw)")[1] + self.assertEqual([line for line in empty.splitlines() if line.startswith("|")], + ["| Library | Name |", "| - | - |"] + + [f"| libSceExample | `{name}` |" for name in "ABCDE"]) + self.assertEqual(progress.compare("Libraries", "Library", "functions", head, head), []) + (library / "Export.cpp").write_text("int APS5_VABI Ready() { return 0; }\n") + with patch.object(progress, "PRX", root): + before = progress.collect_libraries() + (library / "Export.cpp").write_text( + "int APS5_VABI Ready() { return 0; }\nint APS5_VABI F(int a, int b) { return a + b; }\n") + with patch.object(progress, "PRX", root): + after = progress.collect_libraries() + report = "\n".join(progress.compare("Libraries", "Library", "functions", before, after)) + self.assertIn("+1 implemented", report) + self.assertNotIn("behaviourally empty", report) + (library / "Export.cpp").write_text( + "int APS5_VABI Ready() { return 0; }\nint APS5_VABI F(int a, int b) { return a + b; }\nint APS5_VABI A() { return 0; }\n") + with patch.object(progress, "PRX", root): + grown = progress.collect_libraries() + report = "\n".join(progress.compare("Libraries", "Library", "functions", after, grown)) + self.assertIn("+1 implemented", report) + self.assertIn("| libSceExample | `A` |", report) + self.assertIn("⚪ 1 behaviourally empty (constant return or throw)", report) + def test_empty_libraries(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory)