mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-21 20:00:35 +02:00
301 lines
11 KiB
Python
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()
|