guides Intermediate

Error Handling and Diagnostics

How pyvorin-native 1.0.9 fails and reports: the four compile outcomes, verified exit codes, guard failure semantics, and where every diagnostic record lives.

Published Apr 22, 2026

Pyvorin Native fails conservatively by design. When the compiler cannot produce a correct native path, it does not approximate one: it classifies the failure, runs your original Python, and records what happened. Almost nothing in this system is silent, and almost nothing is fatal. This page is the verified error-handling reference for pyvorin-native 1.0.9 — the compile-time taxonomy, the exit codes we measured from the installed CLI, the runtime guard contract, and the exact locations of every diagnostic record the product writes.

The taxonomy of compilation outcomes itself is covered in depth on the compilation failure page; here we focus on what surrounds it — the codes, the records and the decisions.

Exit codes, verified

The CLI's exit codes are the cheapest automation surface it has, so we measured them. All of the following were run against the installed build:

CommandConditionExit code
pyvorin compile f.pyAll functions compiled and validated0
pyvorin compile f.pyAny function FAILED (for example, oracle mismatch)1
pyvorin compile f.py --fail-on-fallbackAll functions COMPILED_FULL with the oracle run0
pyvorin compile f.py --fail-on-fallbackAny function fell back, or compiled with validation skipped ("function requires arguments")2
pyvorin compile f.py --fail-on-fallbackAny function FAILED1
pyvorin explain --strictAny function fails compilationNon-zero

Two catches worth internalising. First, code 2 covers two different situations — a genuine fallback and a "compiled but never validated" function — because the gate treats an unverified compile with the same suspicion as a declined one. A file of functions that all take arguments can compile green yet still exit 2 under the gate, which is the tooling nudging you toward zero-argument entrypoints that the oracle can exercise. Second, a plain compile stays green in that same situation: it exits 0 with Runtime validation not performed noted per function. If your pipeline cares about the difference, the gate flag is the one to wire in.

Compile-time failures

The strongest failure class is the oracle mismatch. In our runs, a generator function using yield produced:

yielder     FAILED   146.227 ms | Oracle mismatch: compiled result differs from CPython

The meaning is exact: the compiler produced native code, ran both it and CPython on the same inputs, and the results disagreed. Rather than ship the doubt, the tooling discards the claim — the function is FAILED, public_claim_allowed is false, and the plain compile exits 1. There is no configuration in 1.0.9 that promotes a mismatched function to compiled; the only route past an oracle mismatch is a code change or a fix from us.

Short of a mismatch, the per-function record carries the reason in structured form. The JSON report (from compile --report path.json) includes, per function: status, error_message, unsupported_features, fallback_used, fallback_reason, cpython_oracle_status (passed, not_run, or a failure note) and public_claim_allowed. For the fallback case, our captured record reads:

"status": "COMPATIBILITY_EXECUTED",
"fallback_used": true,
"fallback_reason": "unsupported AST pattern; calls a compat-mode function",
"cpython_oracle_status": "passed",
"correct": true

Note the combination: the function ran on the interpreter, the result was checked against CPython anyway, and it passed — because the fallback is CPython. "Failed to compile" and "failed to run" are different events in this product, and the report keeps them apart.

Runtime guard failures

Native code runs on assumptions recorded at compile time: argument count, dtypes, memory layout, pointer nullity, aliasing between arrays the kernel assumed were independent. Guards check those assumptions at the boundary before the kernel executes. The contract has three branches, read from the installed router source:

  • Guard fails, Python reference available. The call diverts to a lazily compiled fallback of the original source with the same arguments, and that result is returned. Semantics are preserved exactly because the fallback is the original program.
  • Guard fails, no fallback available. The wrapper raises GuardRuntimeError carrying a structured GuardFailure record — the guard name, a human-readable reason and the argument involved — instead of guessing.
  • Partially applied mutation. When native code has already mutated parameter state that a CPython rerun would double-apply, the wrapper re-raises rather than reruns. A half-applied mutation cannot be replayed honestly, so the system stops loudly.

Two consequences for your error handling. Guard failures do not trigger recompilation in 1.0.9 — a function that fails its dtype guard once will keep failing it on the same call shape, so a rising guard-failure count in your telemetry is a signal to restructure the call, not to wait it out. And because guard failures are recorded — on the execution report's guard_failures list and via the fallback logger — they are observable in exactly the same way as compile-time fallbacks. The full mechanics, including why failed guards do not deoptimise, are on the guards and deoptimisation page.

Unsupported imports

A separate failure class sits at the import boundary. The frontend classifies imports into capability tiers — compile-safe, runtime-callable, compatibility-only and unsupported — and an import in the unsupported tier raises ImportPolicyError at the frontend rather than being silently routed. Unsupported constructs inside a function body behave differently: they never crash the program, they demote the function to the compatibility path and record the reason. The rule of thumb: imports fail fast and loudly, constructs fall back and get logged. If you need strictness at the construct level as well, the strict modes described above are the lever. It is also worth remembering that these two classes meet in the causal chains described on the debugging compiled code page: an unsupported import kills compilation up front, while an unsupported construct demotes the function that contains it and every function that calls it, so the observable symptom of both is often the same COMPATIBILITY_EXECUTED status with different causes behind it.

Licensing failures

The third failure layer is commercial rather than technical, and it fails in one place: the hard gate. When offline validation of the stored lease fails and the online revalidation cannot succeed either, the gate raises LicenseError with both reasons in the message — the local failure and the network failure, so you can tell a corrupted licence file from an unreachable licence server without guessing. pyvorin check runs the same gate from the shell and exits 1 on failure, which is why it belongs in CI and health checks. Diagnosis is local by design: the licence file and entitlement cache under ~/.pyvorin/ are the artefacts to inspect, expiry and device fingerprint are the fields to compare, and PYVORIN_LICENSE_URL is the escape hatch when the server must be reached through a proxy. The full mechanics are on the licence management and billing page.

Where the diagnostics live

When something needs post-hoc investigation, the records are in predictable places:

SurfaceWhat it holds
compile --report path.jsonPer-function status, fallback reason, oracle status, correctness flags.
explain / explain --jsonThe same per-function truth in human or machine form.
pyvorin.diagnostics (fallback logger, fallback events, execution status, compile report)The in-process objects behind those outputs; fallbacks are recorded here as they happen, not reconstructed later.
~/.pyvorin/Licensing state: license.json and the entitlement cache. Licence failures (LicenseError, gate failures) are diagnosed from these files plus pyvorin check.
~/.pyvorin_cache/ and <site-packages>/.pyvorin_cache/disk_compileRuntime and compiled-artefact caches; checksum-validated, so a corrupt artefact is reported on load rather than executed.
Telemetry (standard privacy level)Always-on security and failure events — validation_failed, tamper_detected, revoked, activation — plus sampled compile and execute metadata.

One property ties the whole table together: every failure class writes its record before, or instead of, failing. The design assumption is that you will automate against these records, so they are structured, local and complete. Nothing on this page is reconstructed from memory after the fact; if a record exists, it was written by the code path that failed.

A worked judgement call

Choosing where to be strict is the one decision the tooling cannot make for you. Our guidance, from running these commands in anger: in CI, gate twice. Use --fail-on-fallback on files you have already claimed as native — a regression there is a bug in someone's diff. Use a plain compile on files still in migration, where exiting 2 on every unvalidated helper would train the team to ignore the gate. In production, never gate at all: production takes the fallback, logs the reason, and keeps serving correct results. The exit codes are for pipelines. The fallback is for users. Confuse the two and you will ship either brittleness or slowness.

Where to go next

Last reviewed 22 April 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Every exit code in the table was captured from real runs on 13 September 2026; guard semantics were read from the installed router and FFI sources and cross-checked against the verified architecture report.