feat(tools): mark behaviourally empty bodies in the progress report
Cherry-picked from commit f1be92e95c02be451971fc1a8eec139e738e936d.
This commit is contained in:
parent
0f863c1022
commit
90a82da283
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'<p>Generated by <a href="{SOURCE}/tools/progress.py">tools/progress.py</a> on every push to <code>main</code>. '
|
||||
'Raw numbers: <a href="progress.json">progress.json</a>.</p>',
|
||||
table("System libraries", "Library", libraries),
|
||||
"<p>A function is implemented when it no longer calls <code>NotImplemented_nid_no_patch</code>. "
|
||||
"<p>A function is implemented when it no longer calls <code>NotImplemented_nid_no_patch</code>; "
|
||||
"comparison reports separately mark implemented functions whose body only returns a constant or throws. "
|
||||
f'The total only includes functions already declared in <a href="{SOURCE}/core/libs/prx">core/libs/prx</a>, '
|
||||
"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)
|
||||
|
|
|
|||
|
|
@ -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("<summary>⚪ 5 behaviourally empty (constant return or throw)</summary>", report)
|
||||
empty = report.split("behaviourally empty (constant return or throw)</summary>")[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("<summary>⚪ 1 behaviourally empty (constant return or throw)</summary>", report)
|
||||
|
||||
def test_empty_libraries(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
|
|
|||
Loading…
Reference in New Issue