Files
Cesarus85--ringfall-mjolnir-vr/tools/parked_functions.py
T

301 lines
11 KiB
Python

"""Validate and list functions parked on documented credible residuals.
The manifest is deliberately evidence-only: it does not decide which function
should be worked next. Queue tooling can consume the validated ``active``
keys, while stale measurements fail validation and therefore reopen the
function for investigation.
"""
import argparse
import json
from pathlib import Path
from .coff_compare import (
CoffError,
image_symbol_addresses,
load,
section_info,
section_info_resolved,
section_infos_equal,
)
ALLOWED_CLASSES = {
# The fail-closed classifier found no defensible mechanism. This class is
# intentionally recipe-free: it permits an honest fuzzy park without
# laundering an unknown source/codegen gap into a more specific claim.
"unclassified",
"instruction-scheduling",
"private-register-convention",
"register-allocation",
"tu-context-optimization",
# Not compiler output at all (e.g. vendored __asm bodies); the evidence
# must identify the external origin. Added for matrix4x3_multiply,
# parked in 2fdddb42 with this class but without the allowlist entry.
"vendored-assembly",
# Our source implements the function in inline or __declspec(naked)
# __asm, so it is byte-identical by construction rather than by
# reconstruction. See ASM_IMPLEMENTED_CLASS below: entries in this class
# are expected to compare exact, which is precisely why they are parked.
"asm-implemented",
# The target split and rebuilt object spell one or more relocation targets
# differently even though independently recovered image addresses prove
# identical destinations. See CSPLIT_RELOCATION_ALIAS_CLASS below: these
# entries must differ ordinarily and compare exact only after every
# relocation destination is resolved through unambiguous image evidence.
"csplit-relocation-alias",
}
# The ordinary manifest records functions we cannot yet match. Membership is
# therefore invalidated when a function starts matching. ``asm-implemented``
# inverts that: the body is transcribed assembly, so it always matches, and the
# park records provenance rather than a codegen gap. Exactness must not
# invalidate those entries, and non-exactness must, because a non-matching
# asm body means the transcription has drifted from the target.
ASM_IMPLEMENTED_CLASS = "asm-implemented"
CSPLIT_RELOCATION_ALIAS_CLASS = "csplit-relocation-alias"
class ParkedFunctionsError(RuntimeError):
pass
def _snapshot(info):
return {
"size": info["size"],
"relocation_count": info["relocation_count"],
"normalized_sha256": info["normalized_sha256"],
}
def _differences(expected, current, prefix):
differences = []
for key in ("size", "relocation_count", "normalized_sha256"):
if expected.get(key) != current.get(key):
differences.append({
"measurement": f"{prefix}.{key}",
"expected": expected.get(key),
"current": current.get(key),
})
return differences
def _has_unresolved_relocations(info):
return any(
relocation.get("target", [None])[0] != "address"
for relocation in info.get("relocations", [])
)
def validate_parked_functions(project_root, report_path, config_path, manifest_path):
project_root = Path(project_root)
report = json.loads(Path(report_path).read_text(encoding="utf-8"))
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
manifest = json.loads(Path(manifest_path).read_text(encoding="utf-8"))
if manifest.get("version") != 1 or not isinstance(manifest.get("entries"), list):
raise ParkedFunctionsError("parked manifest must have version 1 and an entries list")
report_units = {unit["name"]: unit for unit in report.get("units", [])}
config_units = {unit["name"]: unit for unit in config.get("units", [])}
seen = set()
active = []
stale = []
invalid = []
for entry in manifest["entries"]:
unit_name = entry.get("unit")
function_name = entry.get("function")
key = f"{unit_name}:{function_name}"
reason = None
if not isinstance(unit_name, str) or not isinstance(function_name, str):
reason = "unit and function must be strings"
elif key in seen:
reason = "duplicate parked-function key"
elif entry.get("class") not in ALLOWED_CLASSES:
reason = "unknown blocker class"
elif not isinstance(entry.get("evidence"), str) or not entry["evidence"].strip():
reason = "non-empty evidence is required"
elif unit_name not in report_units or unit_name not in config_units:
reason = "unit missing from report or objdiff config"
seen.add(key)
if reason:
invalid.append({"key": key, "reason": reason})
continue
functions = [
function for function in report_units[unit_name].get("functions", [])
if function.get("name") == function_name
]
if len(functions) != 1:
invalid.append({
"key": key,
"reason": f"expected one report function, found {len(functions)}",
})
continue
config_unit = config_units[unit_name]
try:
target_object = load(project_root / config_unit["target_path"])
base_object = load(project_root / config_unit["base_path"])
target_info = section_info(target_object, function_name)
base_info = section_info(base_object, function_name)
except (CoffError, KeyError, OSError) as error:
invalid.append({"key": key, "reason": f"cannot measure function: {error}"})
continue
is_exact = section_infos_equal(target_info, base_info)
if entry.get("class") == CSPLIT_RELOCATION_ALIAS_CLASS:
if is_exact:
invalid.append({
"key": key,
"reason": (
"csplit-relocation-alias function does not differ under "
"ordinary comparison"
),
})
continue
try:
symbol_entries = json.loads(
(project_root / "config" / "symbols.json").read_text(
encoding="utf-8"
)
)
if not isinstance(symbol_entries, list):
raise ValueError("symbol manifest must be a list")
symbol_addresses = image_symbol_addresses(symbol_entries)
target_resolved = section_info_resolved(
target_object, function_name, symbol_addresses
)
base_resolved = section_info_resolved(
base_object, function_name, symbol_addresses
)
except (CoffError, KeyError, OSError, TypeError, ValueError) as error:
invalid.append({
"key": key,
"reason": f"cannot resolve csplit relocation alias: {error}",
})
continue
if _has_unresolved_relocations(target_resolved) \
or _has_unresolved_relocations(base_resolved):
invalid.append({
"key": key,
"reason": (
"csplit-relocation-alias requires an unambiguous image "
"address for every relocation destination"
),
})
continue
if not section_infos_equal(target_resolved, base_resolved):
invalid.append({
"key": key,
"reason": (
"csplit-relocation-alias function is not exact after "
"resolving image destinations"
),
})
continue
elif entry.get("class") == ASM_IMPLEMENTED_CLASS:
# Inverted expectation: an asm body matches by construction, so
# only a *non*-matching one is news (the transcription drifted).
if not is_exact:
invalid.append({
"key": key,
"reason": "asm-implemented function no longer matches the target",
})
continue
elif is_exact:
invalid.append({"key": key, "reason": "function is now semantically exact"})
continue
current = {
"target": _snapshot(target_info),
"base": _snapshot(base_info),
"objdiff_percent": functions[0].get("fuzzy_match_percent", 0.0),
}
expected = entry.get("measurements", {})
differences = []
differences.extend(_differences(expected.get("target", {}), current["target"], "target"))
differences.extend(_differences(expected.get("base", {}), current["base"], "base"))
if expected.get("objdiff_percent") != current["objdiff_percent"]:
differences.append({
"measurement": "objdiff_percent",
"expected": expected.get("objdiff_percent"),
"current": current["objdiff_percent"],
})
item = {
"key": key,
"unit": unit_name,
"function": function_name,
"class": entry["class"],
"evidence": entry["evidence"],
"measurements": current,
}
if differences:
item["differences"] = differences
stale.append(item)
else:
active.append(item)
return {
"summary": {
"active": len(active),
"stale": len(stale),
"invalid": len(invalid),
},
"active": active,
"active_keys": [item["key"] for item in active],
"stale": stale,
"invalid": invalid,
}
def require_valid_parked_functions(project_root, report_path, config_path, manifest_path):
result = validate_parked_functions(project_root, report_path, config_path, manifest_path)
if result["stale"] or result["invalid"]:
details = [item["key"] for item in result["stale"] + result["invalid"]]
raise ParkedFunctionsError(
"parked-function evidence is stale or invalid: " + ", ".join(details)
)
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project", type=Path, default=Path.cwd())
parser.add_argument("--report", type=Path, default=Path("build/report.json"))
parser.add_argument("--config", type=Path, default=Path("objdiff.json"))
parser.add_argument("--manifest", type=Path, default=Path("config/parked.json"))
parser.add_argument("--output", type=Path)
parser.add_argument(
"--allow-stale",
action="store_true",
help="report stale/invalid entries without returning a failure status",
)
args = parser.parse_args()
root = args.project.resolve()
result = validate_parked_functions(
root,
root / args.report,
root / args.config,
root / args.manifest,
)
serialized = json.dumps(result, indent=2) + "\n"
if args.output:
output = root / args.output
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(serialized, encoding="utf-8")
print(serialized, end="")
if not args.allow_stale and (result["stale"] or result["invalid"]):
raise SystemExit(1)
if __name__ == "__main__":
main()