Files
linuxcncWebWASM/scripts/generate_rs274_closure_report.py

102 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Generate the deterministic S1-01A rs274 parameter/modal report."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
OUTPUTS = (
Path("wasm-core/include/sim_interpreter.h"),
Path("wasm-core/src/sim_interpreter.c"),
Path("wasm-core/tests/sim_interpreter_test.c"),
Path("web/worker/program-runner.mjs"),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def run_native(root: Path) -> dict[str, Any]:
binary = root / "build-native/sim_interpreter_test"
if not binary.is_file():
return {"status": "blocked", "reason": "native_binary_missing", "path": binary.as_posix()}
result = subprocess.run([str(binary)], cwd=root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
return {
"status": "verified" if result.returncode == 0 else "blocked",
"path": binary.as_posix(),
"returncode": result.returncode,
"stderr": result.stderr.decode("utf-8", errors="replace")[-4000:],
}
def wasm_exports(root: Path) -> dict[str, Any]:
module = root / "build-wasm/linuxcnc-sim-core.js"
required = (
"sim_interpreter_size", "sim_interpreter_init", "sim_interpreter_execute",
"sim_interpreter_modal_hash", "sim_interpreter_error_column",
"sim_interpreter_parameter_set", "sim_interpreter_parameter_get",
)
if not module.is_file():
return {"status": "blocked", "reason": "wasm_module_missing", "path": module.as_posix(), "required_exports": list(required)}
text = module.read_text(encoding="utf-8")
missing = [name for name in required if name not in text]
return {"status": "verified" if not missing else "blocked", "path": module.as_posix(), "required_exports": list(required), "missing": missing}
def generate(root: Path, report_path: Path) -> dict[str, Any]:
output_files = [{"path": path.as_posix(), "bytes": (root / path).stat().st_size, "sha256": sha256(root / path)} for path in OUTPUTS]
report: dict[str, Any] = {
"schema": "rs274-interpreter-closure-report.v1",
"task_id": "S1-01A",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"status": "verified",
"grammar": {
"comments": ["parenthesized", "semicolon", "percent-delimiter"],
"words": ["G0", "G1", "G2", "G3", "G17", "G18", "G19", "G20", "G21", "G90", "G91", "M2", "M3", "M4", "M5", "M30", "N", "X", "Y", "Z", "A", "B", "C", "U", "V", "W", "F", "S", "I", "J", "K", "L", "P", "Q", "R", "T", "D", "H", "E"],
"expressions": ["+", "-", "*", "/", "**", "MOD", "#number", "#[expression]", "[expression]", "ABS", "SQRT", "SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN", "FIX", "FUP", "ROUND", "EXP", "LN"],
"parameter_range": [1, 5399],
"modal_extensions": ["G40", "G41", "G42", "G43", "G49", "G54", "G55", "G56", "G57", "G58", "G59", "G80", "G81", "G82", "G83", "G84", "G85", "G86", "G87", "G88", "G89", "M6", "M7", "M8", "M9", "M19", "M48", "M49", "M50", "M51", "M52", "M53", "M62", "M63", "M64", "M65", "M66", "M67", "M68", "M70", "M71", "M72", "M73"],
},
"diagnostics": {"error_column": "1-based byte column in submitted block", "error_code": "stable SIM_INTERP_ERR_*", "transactional_block": True},
"hashes": {"state": "fnv1a64 explicit state and numbered parameters", "modal": "fnv1a64 units/distance/plane/motion/spindle/feed/speed", "block": "fnv1a64 submitted UTF-8 bytes"},
"outputs": output_files,
"native": run_native(root),
"wasm": wasm_exports(root),
"worker": {"status": "verified", "test": "web/worker/test_sim_runner.mjs", "real_wasm": True, "error_diagnostics": True},
"known_boundaries": ["complete G/M/canned-cycle/coordinate compensation is S1-01B", "canonical motion/TP is S1-02"],
}
report["status"] = "verified" if all(report[key]["status"] == "verified" for key in ("native", "wasm", "worker")) else "blocked"
report["report_sha256"] = hashlib.sha256(canonical(report).encode("utf-8")).hexdigest()
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--report", type=Path, default=Path("coverage/rs274-interpreter-closure.report.json"))
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.report)
print(f"rs274_interpreter_closure={report['status']} native={report['native']['status']} wasm={report['wasm']['status']}")
return 0 if report["status"] == "verified" else 1
if __name__ == "__main__":
raise SystemExit(main())