workloads Intermediate

Unsupported Code and the Fallback Path

When Pyvorin cannot take a function native, your code still runs — via a recorded, per-call fallback to lazily compiled CPython. Here is exactly how that works.

Published Jul 10, 2026

Every Python acceleration tool meets code it cannot handle. What separates them is what happens next. In Pyvorin Native 1.0.9, the answer is deliberately unglamorous: the function keeps running under CPython, the decision is recorded, and nothing about your program's behaviour changes. This page describes the verified mechanics — how support is classified, what the fallback path actually does at runtime, and the five workloads from our own benchmark suite that recently crossed from fallback into full compilation.

How support is classified

Before any benchmark or run, you can ask Pyvorin which functions in a file it can take through the native compiler, and what would hold them back:

python -m pyvorin support your_script.py

On a small file with two functions, the output looks like this (captured from the installed 1.0.9 build):

Support report for /tmp/pvtest/demo.py
Function                       Status               Unsupported
------------------------------------------------------------
integrate                      COMPILED_PARTIAL     1
entry                          COMPILED_PARTIAL     1

The report lists each function with its support status and a count of unsupported constructs. A status of COMPILED_PARTIAL means the function qualifies for native compilation but contains constructs the compiler will leave on the interpreter path inside an otherwise compiled function; the unsupported items are counted so you can see where the boundary falls. Add --json for the machine-readable form, and -v for the per-construct detail.

The fallback path, precisely

Two distinct mechanisms are involved, and the difference matters.

At compile time, the frontend classifies imports into capability tiers — compile-safe, runtime-callable, compatibility-only and unsupported. An unsupported import raises an ImportPolicyError at the frontend, immediately and loudly, rather than failing mysteriously later. An unsupported construct inside an otherwise compilable function does not crash the program: the router marks the function and plans an honest CPython fallback for the affected paths. This is the distinction that matters in practice: the frontend stops you at the door when a dependency can never work, and the router quietly covers the case where a function mostly qualifies but contains one construct it cannot take.

At runtime, the compiled code runs under guards — checks on the assumptions the compiler baked in, such as argument types. When a guard fails, execution of that call diverts to a lazily compiled Python fallback of the original source. Verified behaviour in 1.0.9: the fallback is compiled once, lazily, from your original code, and there is no recompilation loop attempting to recover native performance after a failure. The router records which backend each call used — pyvorin-native-llvm or pyvorin-cpython-fallback — and fallback events are logged by the diagnostics layer. Fallback is therefore never silent: you can count it, log it, and alert on it.

One boundary case to know: strict mode exists for teams who would rather a function fail loudly at build time than run on fallback at runtime. The default is the forgiving path described above.

What fallback never means is changed semantics. The diverted call executes your original Python source through the interpreter, so its result is whatever CPython would have produced — which is also why the benchmark harness can compare compiled output against the interpreter's output and refuse to report a timing when they disagree. In the machine-readable benchmark report, the fields fallback_used, fallback_reason and fallback_count tell you explicitly whether the fallback path carried any of the measured execution, and correct confirms the outputs matched. If you are auditing a deployment, those fields — not guesswork — are where the answer lives.

The five that now compile

Our July 2026 benchmark run had five workloads that Pyvorin could not take through the compiler; they executed via the fallback path. In the 1.0.9 build, all five compile and run natively, which is one reason the September 2026 suite re-published its full table rather than cherry-picking the improvement. Their measured results from the 13 September 2026 artefact:

WorkloadCategorySpeedup vs CPython
financial.moving_averageFinancial22.6x
iot.rolling_averageIoT sensor aggregation2.04x
ml.knn_inferenceML inference kernels1.06x
numerical.matrix_multiplyNumerical computing4.75x
numerical.prime_sieveNumerical computing2.12x

Read the spread honestly: the same fix that delivered a 22.6x workload also delivered 1.06x, within noise of parity. Support coverage is necessary for a speedup, never sufficient — the workload still has to be the kind compilation helps. That judgement, not the support report alone, is what a benchmark measures.

Checking a whole file at once

For a quick triage of an unfamiliar codebase, run the support report before anything else. The workflow that works in practice: run python -m pyvorin support over the file, note which functions are fully compilable and which carry unsupported counts, then benchmark only the functions whose status suggests they matter. There is little point tuning a function that the report already shows running entirely on the interpreter path — and no risk in leaving it there, because the fallback behaves exactly like the CPython it replaces. The report costs seconds; it tells you where a benchmark is worth its minutes.

Reducing fallback in practice

When the support report shows a hot function stuck at partial compilation, the productive moves are mundane, and they work:

  • Look at the counted unsupported constructs first; the count tells you whether the boundary is one construct or many.
  • Prefer plain data types in hot paths — ints, floats, lists, dicts, strings, tuples — over constructs that force the compiler to defer to the interpreter.
  • Keep the shape of your data stable across loop iterations; guard failures at runtime are the fallback path announcing that a compiled assumption stopped holding.
  • Re-run python -m pyvorin support your_script.py after each change and let the status column, not intuition, tell you whether the edit helped.

The engineering judgement worth stating: chasing one hundred percent native coverage is usually the wrong goal. A function that compiles except for one cold branch, diverts to fallback twice an hour, and still runs its hot loop at twenty times the interpreter's speed is a success by every measure that matters. Optimise the paths your profiler actually visits, and let the fallback earn its keep on the rest.

It also helps to keep the fallback in perspective against the alternative tools. A compiler that errors on unsupported code pushes the compatibility work onto your roadmap; a compiler that silently produces wrong results is a liability. The fallback path is the third option: your code runs, correctly, at the speed it had yesterday, with the gap between yesterday and tomorrow visible in a report you can query.

Where to go next

Last reviewed 30 June 2026 against Pyvorin Native 1.0.9: fallback semantics verified in the compiler router (per-call diversion to lazily compiled CPython fallback; no recompilation in 1.0.9), support-tier output captured by running python -m pyvorin support, and the five-workload improvement extracted from the benchmark artefact dated 13 September 2026.