#!/usr/bin/env python3 """Check Capy formatter idempotence and Wasm preservation.""" import argparse import json import pathlib import re import shutil import subprocess import sys import tempfile def command(capyc, args, input_bytes=None): result = subprocess.run( [str(capyc), *map(str, args)], input=input_bytes, capture_output=True, timeout=60, ) if result.returncode: detail = result.stderr.decode(errors="replace").strip() raise RuntimeError(detail or f"capyc exited with {result.returncode}") return result.stdout def wasm(capyc, source, root, output, abi): command(capyc, [source, "-o", output, "--source-map", str(output) + ".source-map", "--abi-version", abi, "--source-root", root]) return pathlib.Path(output).read_bytes() def main(): parser = argparse.ArgumentParser() parser.add_argument("tree", type=pathlib.Path, help="source tree to check") parser.add_argument("--capyc", type=pathlib.Path, default=pathlib.Path("bin/capyc")) parser.add_argument("--allow-backtrace", action="store_true", help="allow the approved site/tests/capy-backtrace.capy exception") args = parser.parse_args() tree = args.tree.resolve() capyc = args.capyc.resolve() if not tree.is_dir(): parser.error(f"source tree does not exist: {tree}") abi_text = (capyc.parent.parent / "src/wasm/abi.h").read_text() abi = re.search(r"#define BEARER_WASM_CORE_ABI_VERSION\s+(\d+)", abi_text)[1] paths = sorted(tree.rglob("*.capy")) counts = {"same": 0, "changed": 0, "exception": 0, "failed": 0} failures = [] with tempfile.TemporaryDirectory(prefix="capy-format-accept-") as scratch_name: scratch = pathlib.Path(scratch_name) work_root = scratch / "source" shutil.copytree(tree, work_root) for relative in (path.relative_to(tree) for path in paths): source = work_root / relative original_bytes = source.read_bytes() try: formatted_bytes = command(capyc, ["--format", "-"], input_bytes=original_bytes) if command(capyc, ["--format", "-"], input_bytes=formatted_bytes) != formatted_bytes: raise RuntimeError("formatter is not idempotent") output = scratch / "unit.wasm" before = wasm(capyc, source, work_root, output, abi) after = before if original_bytes != formatted_bytes: source.write_bytes(formatted_bytes) after = wasm(capyc, source, work_root, output, abi) if before != after: if args.allow_backtrace and relative.as_posix() == "tests/capy-backtrace.capy": counts["exception"] += 1 print(f"EXCEPTION {relative}: embedded source locations change backtrace bytes") else: raise RuntimeError("Wasm differs") elif original_bytes == formatted_bytes: counts["same"] += 1 else: counts["changed"] += 1 except Exception as error: counts["failed"] += 1 failures.append((relative.as_posix(), str(error))) print(f"FAIL {relative}: {error}") finally: source.write_bytes(original_bytes) summary = {"count": len(paths), "counts": counts, "failures": failures} print(json.dumps(summary, indent=2)) return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())