From e636fc399addbc56c85d7cb35fdadde79d1de940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Sat, 14 Nov 2026 12:26:58 +0100 Subject: [PATCH 1/2] test(runtime): add executable compatibility contracts --- docs/RUNTIME_CONTRACTS.md | 121 ++++++++++++++ scripts/test-runtime-contracts-runner.py | 82 ++++++++++ scripts/test-runtime-contracts.py | 198 +++++++++++++++++++++++ tests/README.md | 17 ++ 4 files changed, 418 insertions(+) create mode 100644 docs/RUNTIME_CONTRACTS.md create mode 100644 scripts/test-runtime-contracts-runner.py create mode 100644 scripts/test-runtime-contracts.py diff --git a/docs/RUNTIME_CONTRACTS.md b/docs/RUNTIME_CONTRACTS.md new file mode 100644 index 0000000..147b259 --- /dev/null +++ b/docs/RUNTIME_CONTRACTS.md @@ -0,0 +1,121 @@ +# Executable runtime compatibility contracts + +This index assigns each compatibility fact to an existing Erlang fixture. The +focused runner executes those assertions unchanged on native Nera (default and +one scheduler) and Erlang/OTP 29. It changes checking, not product behavior or +language support. This is the first guardrail for incremental compiler/runtime +refactoring, not an exception-routing refactor or a claim of full BEAM parity. + +## Contract index + +| Owning fixture | Semantic responsibility and intent source | Assertions and boundary | +| --- | --- | --- | +| [`try_operation_short_circuit`](../tests/try_operation_short_circuit.erl) | [Immediate failure transfer](https://github.com/processone/nera/commit/4e4dec124e0ecf244bd5c7ed77b3da81d29ab48a): the first raising operation stops later body effects. | Checks handler class/reason, mailbox and process-dictionary effects, guards, nested handlers, cleanup and unprotected operations. Its existing exit-reason projection does **not** establish exit-envelope or stack metadata equality. | +| [`try_after_reraise`](../tests/try_after_reraise.erl) | The fixture's stated regression: `after` runs on all exits and re-raises the held exception; cleanup exceptions replace it. | Checks all three classes, exactly-once cleanup, normal returns, stack preservation within each engine, suspended cleanup and garbage collection. Stack preservation does not mean native and BEAM traces have identical metadata. | +| [`error_2`](../tests/error_2.erl) | The fixture's stated intent: `error/2` raises the supplied reason through direct, tail, apply, fun and computed-module calls. | Checks exact class/reason, non-returning behavior, catch envelope and list-shaped traces. Does not assert retention of `Args`, file metadata or `error_info`. | +| [`float_arith_badarith`](../tests/float_arith_badarith.erl) | The fixture's stated regression: non-finite arithmetic and unrepresentable float operands raise `badarith`; earlier false filters stop evaluation. | Checks zero divisors, overflow, large operands, literal folds, finite results, signed zero/underflow, guard rejection and side effects. Existing uncaught-reason projection does not prove exit-envelope parity or numeric-conversion compatibility. | +| [`bin_comp_badarg`](../tests/bin_comp_badarg.erl) | The fixture's stated regression: each binary-comprehension body must be a bitstring at that iteration, not an iolist. | Checks invalid bodies, exact class/reason, preserved body exceptions, valid controls and ordered filter/body effects. The existing monitor assertion accepts two exit-envelope shapes; this runner does not strengthen or normalize them. | +| [`resume_exception`](../tests/resume_exception.erl) | [Suspended exception repair](PROJECT_STATUS.md#p0-1--exception-handlers-lost-across-suspension--repaired): handlers must survive receive suspension. | Checks exit/throw/error, nested callers, absent later dictionary effects, cleanup, message wakeup and normal return. Does not yet own the later arithmetic-context suspension pilot. | + +Each fact stays in its owning fixture. The runner adds no second implementation +of Erlang semantics and performs no textual equality comparison of crash reports, +process IDs, generated helper names, paths or complete printed terms. It adds +no stack normalization and weakens no assertion. The documented inherited +boundaries are not passing assertions for tracked incompatibilities. Future +stack/envelope work should use its owning stricter regressions, not this corpus +as a substitute. + +## Run the contracts + +Build both artifacts from the same source, runtime first, with the pinned +Rust toolchain and lockfile: + +```sh +cargo build --release --locked -p nera-runtime +cargo build --release --locked -p nera +python3 scripts/test-runtime-contracts.py \ + --compiler-source "$(git rev-parse HEAD)" --out /external/results/contracts.json +python3 scripts/test-runtime-contracts-runner.py \ + --out /external/results/contract-rejections.json +``` + +`--nera` and `--runtime` select other matching artifacts; the runtime defaults +beside the compiler. `--temp-parent` selects disposable storage; all compiled +programs are removed after execution. `--timeout` bounds each program (30 seconds +by default); compilation has a separate 120-second limit. Runs are serial. +`--fixture NAME` is a diagnostic subset, explicitly marked `full_corpus: false`; +it does not replace the complete acceptance run. + +The runner copies the compiler and archive together into disposable storage, +compiles outside the checkout, and requires the native linker to report that +exact archive. The supplied source commit is explicitly **declared**, not inferred +from a version string: artifact hashes and observed linkage identify what ran, +but cannot authenticate the caller's build provenance. Retain ordered build logs +with the JSON, particularly for historical artifacts. Fixture source identity, +fixture hashes and fixture-checkout modifications are recorded separately. + +The OTP probe reads `releases/29/OTP_VERSION` from the running installation; +a major-only version is insufficient. BEAM compilation uses that same running +OTP installation's `compile:file/2`, not an independently resolved compiler. +The compiler version, exact OTP patch, +platform, scheduler mode, full commands, working directories, elapsed times, +stdout, stderr, exit status, compilation status and completion are machine-readable. +Inherited native/OTP scheduler flags are cleared; `native-single` explicitly sets +`NERA_SCHEDULERS=1`. The other two modes use their default scheduler counts. + +Each native and BEAM build uses a small entry module that requires `ok = test()` +and prints a second completion line only afterward. Success requires all of: + +1. Compilation and execution actually happened. +2. The real fixture's assertions returned `ok` and the program exited zero. +3. The fixture's own `EXPECT` line and the entry module's return marker each + appeared exactly once. + +Missing commands, unsupported OTP, failed compilation, assertion errors, signals, +timeouts, missing execution and missing completion fail the command. JSON is +written for failed runs too. Outputs are not compared wholesale or normalized. + +## Rejection and historical proof + +`test-runtime-contracts-runner.py` uses disposable copies of the real `error_2` +fixture. Its unchanged control must pass in all three modes. Removing only the +printed marker must fail despite exit zero and a completed return. Changing its +expected reason to a deliberately incorrect atom must produce an actual assertion +failure in every mode. Exiting zero immediately after the fixture marker must +also fail because the entry module's return marker is absent. Compilation failure is not accepted as negative proof. +No tracked fixture or expectation is edited. + +To demonstrate an owning regression, build the source immediately before the +immediate-failure-transfer repair, commit +`a47a763f6257ba1a48a161a6d3eb50eef4fae3fb`, with **its own runtime first**, then +run the current unchanged `try_operation_short_circuit` through the focused runner: + +```sh +python3 scripts/test-runtime-contracts.py \ + --nera /external/historical/target/release/nera \ + --runtime /external/historical/target/release/libnera_runtime.a \ + --compiler-source a47a763f6257ba1a48a161a6d3eb50eef4fae3fb \ + --fixture try_operation_short_circuit \ + --out /external/results/historical-contract.json +``` + +The historical native execution must fail the fixture's assertions, while OTP +passes; a missing tool or compilation failure is not the required regression +proof. Do not substitute today's runtime for the historical archive. + +## Gate and coverage limits + +Run the complete focused corpus and rejection journey when their coverage changes, +and repeat changed test code ten consecutive times without retries. These checks +do not replace the full current [contributing gate](../CONTRIBUTING.md#running-tests), +strict lint, workspace tests, full serial native suite, stacktrace/allocator probes, +applicable integration/platform journeys, independent review or hosted checks. +The workflow configuration is unchanged. Performance probes for allocator, timer +and shutdown changes follow their own contributing-guide conditions. + +A passing result proves only the assertions listed above on the recorded engine, +artifacts and platform. macOS native execution does not prove Linux or Windows +execution; cross-object generation does not prove target-native execution. +No new architecture, performance result or broader language support follows from +these contracts. Refactoring arithmetic exception routing remains a separate +change after these guardrails merge. diff --git a/scripts/test-runtime-contracts-runner.py b/scripts/test-runtime-contracts-runner.py new file mode 100644 index 0000000..47f66bd --- /dev/null +++ b/scripts/test-runtime-contracts-runner.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Reject incomplete execution and failed assertions using real error_2 copies. + +These disposable mutations test the runner, not alternative language semantics. +The tracked fixture and all its expectations remain unchanged. +""" +import argparse +import importlib.util +import json +from pathlib import Path +import shutil +import tempfile + + +SPEC = importlib.util.spec_from_file_location( + "runtime_contracts", Path(__file__).with_name("test-runtime-contracts.py")) +contracts = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(contracts) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--nera", type=Path, default=contracts.ROOT / "target/release/nera") + parser.add_argument("--runtime", type=Path) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--temp-parent", type=Path) + args = parser.parse_args() + compiler = args.nera.resolve(strict=True) + runtime = (args.runtime or compiler.with_name("libnera_runtime.a")).resolve(strict=True) + original = (contracts.ROOT / "tests/error_2.erl").read_text(encoding="utf-8") + variants = { + "control": original, + "missing-marker": original.replace('io:format("error_2_ok~n")', "ok"), + "early-exit": original.replace('io:format("error_2_ok~n")', 'io:format("error_2_ok~n"), halt(0)'), + "assertion": original.replace("{Name, {error, x}} =", "{Name, {error, contract_wrong_reason}} ="), + } + outcomes = {} + with tempfile.TemporaryDirectory(prefix="nera contract rejection ", dir=args.temp_parent) as temporary: + root = Path(temporary) + tools = root / "tools" + tools.mkdir() + isolated_compiler = tools / compiler.name + isolated_runtime = tools / runtime.name + shutil.copy2(compiler, isolated_compiler) + shutil.copy2(runtime, isolated_runtime) + for name, text in variants.items(): + folder = root / name + folder.mkdir() + source = folder / "error_2.erl" + source.write_text(text, encoding="utf-8") + result = contracts.run_contract(source, isolated_compiler, isolated_runtime, + "erl", folder, 30) + outcomes[name] = result + # A marker-only negative must actually return successfully in all modes; + # a compile error is not evidence that completion checking works. + control = outcomes["control"]["passed"] + missing = outcomes["missing-marker"]["runs"] + assertion = outcomes["assertion"]["runs"] + early = outcomes["early-exit"]["runs"] + checks = { + "control": control, + "missing-marker": len(missing) == len(contracts.MODES) and all( + r.get("returncode") == 0 and r.get("return_completed") + and not r.get("fixture_completed") and not r["passed"] for r in missing), + "early-exit": len(early) == len(contracts.MODES) and all( + r.get("returncode") == 0 and r.get("fixture_completed") + and not r.get("return_completed") and not r["passed"] for r in early), + "assertion": len(assertion) == len(contracts.MODES) and all( + r.get("returncode") not in (None, 0) and not r.get("return_completed") + and not r["passed"] and "badmatch" in r.get("stderr", "") + r.get("stdout", "") + for r in assertion), + } + report = {"passed": all(checks.values()), "checks": checks, "outcomes": outcomes, + "compiler_sha256": contracts.digest(compiler), "runtime_sha256": contracts.digest(runtime)} + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + print(json.dumps(checks, sort_keys=True)) + return 0 if report["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test-runtime-contracts.py b/scripts/test-runtime-contracts.py new file mode 100644 index 0000000..079b8f0 --- /dev/null +++ b/scripts/test-runtime-contracts.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Execute the fixture-owned runtime contracts; see docs/RUNTIME_CONTRACTS.md. + +Build runtime before compiler. This is a focused journey, not the project gate. +All process output, including failures and incomplete execution, is kept in JSON. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import re +import shutil +import subprocess +import tempfile +import time + + +ROOT = Path(__file__).resolve().parents[1] +CONTRACTS = ( + "try_operation_short_circuit", "try_after_reraise", "error_2", + "float_arith_badarith", "bin_comp_badarg", "resume_exception", +) +MODES = ("native-default", "native-single", "beam") +DONE = "runtime_contract_returned_ok" + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def execute(command, cwd, env, timeout): + """A missing command, timeout or signal is an outcome, never a skip.""" + record = {"command": [str(x) for x in command], "cwd": str(cwd), + "returncode": None, "completed": False, "failure": None} + start = time.monotonic() + try: + result = subprocess.run(record["command"], cwd=cwd, env=env, + capture_output=True, text=True, timeout=timeout) + record.update(returncode=result.returncode, completed=True, + stdout=result.stdout, stderr=result.stderr) + if result.returncode != 0: + record["failure"] = "nonzero exit" + except subprocess.TimeoutExpired as error: + def text(value): + return value.decode(errors="replace") if isinstance(value, bytes) else value or "" + record.update(failure="timeout", stdout=text(error.stdout), stderr=text(error.stderr)) + except OSError as error: + record.update(failure=str(error), stdout="", stderr="") + record["seconds"] = time.monotonic() - start + return record + + +def environment(): + env = os.environ.copy() + # Do not let caller settings silently turn default into single scheduler, + # change the OTP invocation or load arbitrary modules via Erlang flags. + for key in ("NERA_SCHEDULERS", "ERL_FLAGS", "ERL_AFLAGS", "ERL_ZFLAGS", "ERL_LIBS"): + env.pop(key, None) + return env + + +def run_contract(source, compiler, runtime, erl, work, timeout): + """Compile the unchanged source and assert its return plus its own EXPECT.""" + module = source.stem + text = source.read_text(encoding="utf-8") + markers = re.findall(r"^%% EXPECT: (.+)$", text, re.MULTILINE) + result = {"fixture": module, "sha256": digest(source), "builds": [], "runs": [], + "passed": False, "failures": []} + if len(markers) != 1: + result["failures"].append("fixture must own exactly one EXPECT completion marker") + return result + marker = markers[0] + result["completion_marker"] = marker + wrapper = work / "runtime_contract_entry.erl" + wrapper.write_text( + "-module(runtime_contract_entry).\n-export([main/0]).\n" + f'main() -> ok = {module}:test(), io:format("{DONE}~n"), ok.\n', encoding="utf-8") + binary = work / (module + (".exe" if os.name == "nt" else ".native")) + env = environment() + builds = [ + ("native", [compiler, "build", source, wrapper, "-e", "runtime_contract_entry:main", "-o", binary]), + ("beam", [erl, "-noshell", "-eval", + 'lists:foreach(fun(P) -> R=compile:file(P,[{outdir,' + json.dumps(str(work)) + + '},return_errors,return_warnings]), io:format("~p~n",[R]), ' + 'case R of {ok,_,_} -> ok; _ -> halt(1) end end, [' + + ','.join(json.dumps(str(p)) for p in (source, wrapper)) + ']), halt(0).']), + ] + ready = {} + for engine, command in builds: + record = execute(command, work, env, 120) + record["engine"] = engine + # Prove the native build used the supplied archive, not another checkout + # or an embedded runtime from a different compiler version. + if engine == "native" and not record["failure"]: + linked = f"Linking with runtime: {runtime}" + if linked not in record["stderr"].splitlines(): + record["failure"] = "supplied runtime linkage was not observed" + ready[engine] = not record["failure"] + result["builds"].append(record) + for mode in MODES: + engine = "beam" if mode == "beam" else "native" + if not ready[engine]: + result["runs"].append({"mode": mode, "completed": False, "passed": False, + "failure": "compilation did not succeed; execution missing"}) + continue + run_env = env.copy() + if mode == "native-single": + run_env["NERA_SCHEDULERS"] = "1" + command = ([erl, "-noshell", "-pa", work, "-eval", + "runtime_contract_entry:main(), halt(0)."] + if engine == "beam" else [binary]) + record = execute(command, work, run_env, timeout) + record.update(mode=mode, scheduler="1" if mode == "native-single" else "default") + lines = record["stdout"].splitlines() + record["fixture_completed"] = lines.count(marker) == 1 + record["return_completed"] = lines.count(DONE) == 1 + if not record["failure"] and not (record["fixture_completed"] and record["return_completed"]): + record["failure"] = "missing or duplicate completion marker" + record["passed"] = not record["failure"] + result["runs"].append(record) + result["failures"] = [f'{r["mode"]}: {r["failure"]}' for r in result["runs"] if not r["passed"]] + result["passed"] = len(result["runs"]) == len(MODES) and not result["failures"] + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--nera", type=Path, default=ROOT / "target/release/nera") + parser.add_argument("--runtime", type=Path, help="matching archive; defaults beside compiler") + parser.add_argument("--compiler-source", required=True, + help="full source commit used to build BOTH supplied artifacts") + parser.add_argument("--erl", default="erl") + parser.add_argument("--out", required=True, type=Path) + parser.add_argument("--temp-parent", type=Path) + parser.add_argument("--timeout", type=float, default=30) + parser.add_argument("--fixture", choices=CONTRACTS, action="append", + help="diagnostic subset; JSON distinguishes it from the full corpus") + args = parser.parse_args() + report = {"schema": 1, "passed": False, "failures": [], "contracts": [], + "platform": platform.platform(), "selected": args.fixture or list(CONTRACTS), + "full_corpus": not args.fixture or set(args.fixture) == set(CONTRACTS), + "compiler_source_declared": args.compiler_source} + try: + if not re.fullmatch(r"[0-9a-f]{40}", args.compiler_source): + raise ValueError("compiler source must be a full commit ID") + if args.timeout <= 0: + raise ValueError("timeout must be positive") + compiler = args.nera.resolve(strict=True) + runtime = (args.runtime or compiler.with_name( + "nera_runtime.lib" if os.name == "nt" else "libnera_runtime.a")).resolve(strict=True) + report["compiler"] = {"path": str(compiler), "sha256": digest(compiler)} + report["runtime"] = {"path": str(runtime), "sha256": digest(runtime)} + env = environment() + # Always capture the fixture checkout identity separately from the + # declared compiler identity (important for historical rejection runs). + report["fixture_source"] = execute(["git", "rev-parse", "HEAD"], ROOT, env, 10) + report["fixture_changes"] = execute(["git", "status", "--porcelain", "--", "tests"], ROOT, env, 10) + for name in ("fixture_source", "fixture_changes"): + if report[name]["failure"]: + raise ValueError(f"cannot record {name}") + with tempfile.TemporaryDirectory(prefix="nera runtime contracts ", dir=args.temp_parent) as temporary: + work = Path(temporary) + tools = work / "tools" + tools.mkdir() + isolated_compiler = tools / compiler.name + isolated_runtime = tools / runtime.name + shutil.copy2(compiler, isolated_compiler) + shutil.copy2(runtime, isolated_runtime) + report["compiler"]["version"] = execute([isolated_compiler, "--version"], work, env, 10) + otp_query = ('P=filename:join([code:root_dir(),"releases",' + 'erlang:system_info(otp_release),"OTP_VERSION"]),' + '{ok,V}=file:read_file(P),io:format("~s",[V]),halt(0).') + report["otp"] = execute([args.erl, "-noshell", "-eval", otp_query], work, env, 30) + if report["compiler"]["version"]["failure"]: + raise ValueError("compiler version probe failed") + if report["otp"]["failure"] or not re.fullmatch(r"29\.\d+(?:\.\d+)*", report["otp"]["stdout"].strip()): + raise ValueError("a recorded OTP 29 patch version is required") + report["otp"]["patch"] = report["otp"]["stdout"].strip() + for name in report["selected"]: + folder = work / name + folder.mkdir() + report["contracts"].append(run_contract( + ROOT / "tests" / f"{name}.erl", isolated_compiler, isolated_runtime, + args.erl, folder, args.timeout)) + report["failures"] = [failure for contract in report["contracts"] for failure in contract["failures"]] + report["passed"] = bool(report["contracts"]) and all(c["passed"] for c in report["contracts"]) + except (OSError, ValueError) as error: + report["failures"].append(str(error)) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + print(f'Runtime contracts: {"passed" if report["passed"] else "FAILED"}; results: {args.out}') + return 0 if report["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/README.md b/tests/README.md index f624a1c..976fbaf 100644 --- a/tests/README.md +++ b/tests/README.md @@ -34,6 +34,23 @@ The legacy shell script `./tests/run_tests.sh` is still available but `nera test is the recommended runner — it runs tests in parallel, has colored output, and produces a failure summary. +## Focused runtime contracts + +The [runtime contract index](../docs/RUNTIME_CONTRACTS.md) names the semantic +responsibility and limits of six existing fixtures. Run their assertions +unchanged on native default/single schedulers and a recorded OTP 29 patch: + +```sh +python3 scripts/test-runtime-contracts.py \ + --compiler-source "$(git rev-parse HEAD)" --out /external/results/contracts.json +python3 scripts/test-runtime-contracts-runner.py \ + --out /external/results/contract-rejections.json +``` + +Build the matching runtime before the compiler. These focused journeys retain +exit, assertion and completion evidence in JSON; they do not replace the full +project gate or establish compatibility beyond their owning assertions. + ## Project EUnit configuration A project can select EUnit in `nera.toml`: From 7f89b7de91783ca225d9ab4407db09997ae98f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Sat, 14 Nov 2026 12:35:33 +0100 Subject: [PATCH 2/2] fix(tests): preserve canonical and Unicode contract paths --- scripts/test-runtime-contracts-runner.py | 2 +- scripts/test-runtime-contracts.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/test-runtime-contracts-runner.py b/scripts/test-runtime-contracts-runner.py index 47f66bd..607be52 100644 --- a/scripts/test-runtime-contracts-runner.py +++ b/scripts/test-runtime-contracts-runner.py @@ -36,7 +36,7 @@ def main(): } outcomes = {} with tempfile.TemporaryDirectory(prefix="nera contract rejection ", dir=args.temp_parent) as temporary: - root = Path(temporary) + root = Path(temporary).resolve() tools = root / "tools" tools.mkdir() isolated_compiler = tools / compiler.name diff --git a/scripts/test-runtime-contracts.py b/scripts/test-runtime-contracts.py index 079b8f0..7bb2e54 100644 --- a/scripts/test-runtime-contracts.py +++ b/scripts/test-runtime-contracts.py @@ -82,10 +82,10 @@ def run_contract(source, compiler, runtime, erl, work, timeout): builds = [ ("native", [compiler, "build", source, wrapper, "-e", "runtime_contract_entry:main", "-o", binary]), ("beam", [erl, "-noshell", "-eval", - 'lists:foreach(fun(P) -> R=compile:file(P,[{outdir,' + json.dumps(str(work)) + + 'lists:foreach(fun(P) -> R=compile:file(P,[{outdir,' + json.dumps(str(work), ensure_ascii=False) + '},return_errors,return_warnings]), io:format("~p~n",[R]), ' 'case R of {ok,_,_} -> ok; _ -> halt(1) end end, [' + - ','.join(json.dumps(str(p)) for p in (source, wrapper)) + ']), halt(0).']), + ','.join(json.dumps(str(p), ensure_ascii=False) for p in (source, wrapper)) + ']), halt(0).']), ] ready = {} for engine, command in builds: @@ -161,7 +161,7 @@ def main(): if report[name]["failure"]: raise ValueError(f"cannot record {name}") with tempfile.TemporaryDirectory(prefix="nera runtime contracts ", dir=args.temp_parent) as temporary: - work = Path(temporary) + work = Path(temporary).resolve() tools = work / "tools" tools.mkdir() isolated_compiler = tools / compiler.name