Skip to main content

Runtime Tiers and Profiles

Three independent axes shape how a Verum program runs. They are often conflated; this page keeps them separate.

AxisWhat it selectsWhere it lives
Execution modeinterpreted VBC vs ahead-of-time-compiled native--interp / --aot, [codegen].tier
CBGR safety tierhow much runtime memory-safety checking is emittedReference syntax and compiler analysis
Runtime profilewhich subset of the runtime your target can support@cfg(runtime = "...") at build time

The remainder of the page treats each axis in turn and then describes how they combine.

Axis 1 — Execution mode​

Verum offers VBC interpretation and ahead-of-time native compilation. verum check checks source without executing it; it is not a third execution tier.

ModeCommandResult
Interpreterverum run --interp app.vrCompile to VBC and execute it in the interpreter.
AOTverum run --aot app.vrCompile a native executable through LLVM and run it.
Check onlyverum check app.vrReport source diagnostics without running the program.

For project execution, an explicit CLI mode takes precedence over [codegen].tier. The default project value is "aot", including when the section or its tier key is omitted. Set tier = "interpret" or pass --interp to run a project through the interpreter. --release selects build settings; it does not override the configured execution mode.

An explicit file invocation such as verum run app.vr defaults to the interpreter. Use explicit flags when comparing backends.

Known limitation: interpreter and native behaviour are not fully interchangeable. Generic callable chains, imported type information, panic handling, and owned-resource cleanup still have failing cases. A successful interpreter run does not validate the native program. Pending compiler changes are not a released parity guarantee.

Interpreter — VBC​

The VBC interpreter executes bytecode without an LLVM compilation step. It is useful for interactive development, the REPL, and source-level checks. Startup and execution cost depend on the program and the amount of standard-library code it loads.

The interpreter declares its trust assumption at construction. A lenient load is for bytecode that just came out of the compiler in this process; a validated load runs the per-instruction bytecode validator plus content- and dependency-hash verification on bytecode coming from disk, network, or a shared cog archive. See VBC Bytecode → Module-load trust boundary.

Native intercept layer​

The interpreter resolves certain stdlib calls natively before running their bytecode bodies. Two surface classes flow through this layer:

  • Syscall-shaped surfaces — file I/O, env-var ops, stdin / stdout, shell-process spawn, networking. The interpreter services these calls through host-side runtime handlers. Some use Rust std and libc, including networking. This is permitted for the host interpreter, subject to clean-target CLI compatibility and the same API semantics as AOT. Native interception does not mean a direct syscall; see the separate host and AOT contracts.
  • Canonical stdlib factories — well-known constructors whose contract is fixed (Path.new, Text.new, Text.with_capacity, Text.from_str, Text.from_char, …). The interpreter returns the canonical value without re-walking the stdlib body.

Surfaces currently covered by the native layer:

SurfaceExamples
Shellsh#"...", sh_check, sh
File systemcore.io.fs.* free functions
Environmentcore.env.var, set_var, remove_var
Stdinread_line, read_int, read_to_end
Path / PathBufPath.new, PathBuf.from, inherent path methods
Text factoriesText.new, Text.with_capacity, Text.from_str, …
ProcessProcess.spawn, Command.*

These intercepts are intended to implement the same API contracts as the bytecode bodies. An interpreter result can nevertheless exercise a different implementation from native code; validate both when the distinction matters.

Interpreter fallback set

By design, the interpreter returns NotImplemented for three feature families that require native toolchains:

  • GPU dispatch — actual @device(GPU) kernel launch. Use verum run --aot --gpu or verum build with the appropriate GPU backend (metal, cuda, rocm, vulkan).
  • Dynamic FFI resolution — dlopen/LoadLibrary-style symbol lookup. Static extern "C" declarations resolved at link time work in AOT.
  • ML vmap / pmap — vectorised/parallel-mapped kernel JITs. Use AOT with the tensor-op compilation path.

Other features also require backend-specific validation. In particular, see the current async evaluation contract.

AOT — LLVM​

The native path lowers VBC through LLVM and links an executable. Use it to assess native deployment, code generation, and resource behaviour. LLVM optimisation, target support, and link configuration are described in codegen.

Verified behaviour: when AOT execution is requested explicitly, a compilation failure returns an error instead of running the source through the interpreter. For native acceptance, check the exit status and diagnostics, and confirm that a fresh compiled executable ran. Output from an interpreter fallback in an older build does not validate the native path.

Dual-path compilation (CPU vs GPU)​

MLIR is not a tier. It is used only for the GPU path. CPU code lowers directly through LLVM:

See codegen for the MLIR dialect stack and per-target tile sizes.

Why two execution modes​

Interpretation provides a development path without native compilation; AOT produces an executable for the target platform. These are the supported CLI execution choices. Their availability does not imply that every language feature has identical backend coverage.

An experimental MLIR JIT is retained for internal development. It is not a third --tier choice. See codegen for the compiler paths and target backends.

Axis 2 — CBGR safety tiers​

Reference safety is independent of interpreter versus native execution. The compiler's ReferenceTier and CbgrTier use three variants:

ReferenceCompiler tierSafety obligation
&T, &mut TTier 0Runtime CBGR validation where static analysis cannot eliminate it.
&checked T, &checked mut TTier 1Compiler-proven lifetime and access validity.
&unsafe T, &unsafe mut TTier 2The programmer supplies the safety argument.

An execution tier named “Tier 1” therefore means native compilation, while a reference tier named “Tier 1” means a compiler-proven reference. Selecting AOT does not turn all references into checked references.

The standard library also has a build-wide policy enum in core/runtime/env.vr, exposed through MemoryContext.cbgr_tier. Its four names (Tier0_Full, Tier1_Epoch, Tier2_Gen, and Tier3_Unchecked) belong to that runtime structure. They are not the compiler's per-reference choices and should not be used to interpret &checked T or &unsafe T.

Tier selection in the compiler​

CBGR analysis considers escape, lifetime, aliasing, and control-flow facts. A managed reference can be promoted when those facts prove its uses safe. Inconclusive analysis keeps runtime checking.

&checked T expresses a proof requirement; &unsafe T transfers the obligation to the programmer. Neither annotation is an ownership or resource-cleanup guarantee. See references for the syntax and the current lifetime limitations.

Axis 3 — Runtime profiles​

The third axis names which subset of the runtime a piece of code is intended to use. The standard library marks these groups with @cfg(runtime = "…") attributes.

Runtime profile selection

Known limitation: the attributes are written and the stdlib is organised by them. There is no way to select a profile yet. Verified: @cfg(runtime = "X") is not a known cfg key — TargetConfig::matches handles target_os, target_arch, target_family, target_pointer_width, target_endian, target_env, target_vendor and feature, and falls through to a custom map for everything else. That map is populated only by CompilerOptions::with_cfg_custom, which nothing in verum_cli calls; there is no --cfg flag, and BuildConfig — the [build] manifest section — has no runtime field (its fields are target, opt_level, incremental, lto, codegen_units, panic, subsystem).

The one place the value has an effect today is the stdlib bake: DEFAULT_EXCLUDED_RUNTIMES = ["embedded", "none"] in verum_compiler::module_utils drops the files carrying those two, matching the attribute by NAME rather than evaluating it — which is also why the other four stay in.

So read the table below as the intended taxonomy, and the paragraph after it about build-time refusal as intent rather than behaviour.

ProfileExecutorHeapThreadsI/O driverUse case
fullWork-stealingSystem allocatorOS threadsio_uring / kqueue / IOCPServers, CLIs, desktop apps
single_threadCooperativeSystem allocatorCurrent onlySingle-threaded reactorWASM, browser, embedded single-core
no_async—System allocatorOS threadsBlocking POSIX / WindowsBatch tools, CLIs without futures
no_heapCooperativeStack onlyCurrent onlyPolling onlyReal-time, safety-critical
embedded—Stack onlyCurrent onlyMMIO / registers onlyMicrocontrollers, freestanding
none—Stack onlyCurrent onlyNoneNo runtime at all — core/sys/no_runtime.vr

INTENDED, not yet built: a program that uses a feature its profile does not support should be a build-time error rather than a runtime failure — verum build refusing to link runtime = "no_heap" against any transitive dependency that allocates from Heap<T>. Nothing implements that check today.

The [build] runtime = "…" key that earlier versions of this page showed does not exist; writing it into verum.toml selects nothing. Nor is there a core/stack module for no_heap to fall back to — core/collections is not excluded by any profile.

Async scheduler​

The runtime uses a work-stealing executor:

  • Per-core worker threads: number = num_cpus() by default (async_worker_threads = 0 in [runtime]).
  • Per-worker deque: local tasks pushed / popped LIFO; stolen FIFO.
  • Global queue: for tasks spawned from outside the executor.
  • IO reactor: one thread driving io_uring (Linux) / kqueue (macOS/BSD) / IOCP (Windows).

The task runtime carries execution context across task boundaries. Keep this distinct from direct async fn evaluation: an eager call does not become a suspendable task just because it appears next to .await. See async and concurrency.

Memory: unified CBGR arena​

The memory-management model uses CBGR metadata to track allocations and reference validity. Sharing that model does not make arbitrary interpreter and native values ABI-compatible. Use the declared embedding or FFI boundary when crossing runtimes. See allocation internals.

Selecting the execution mode​

Per-invocation​

verum run --interp app.vr
verum run --aot app.vr
verum run --tier interpret app.vr
verum run --tier aot app.vr
verum check app.vr
verum build --release # build the current project

--interp, --aot, and --tier are alternative selectors. Accepted --tier names are interpret (also interpreter) and aot. --tier check is rejected: use the check command.

Per-project (verum.toml)​

[codegen]
tier = "aot" # interpret | aot for verum run

An explicit CLI selector overrides this value. Omitting the section or the tier key still selects its default, "aot". The older [profile.dev].tier and [profile.release].tier settings are consulted only when the effective codegen value is not recognised; omitting the key does not activate that fallback. [build].tier is not the execution-mode setting.

Although the configuration model also recognises "check", verum run refuses [codegen].tier = "check" because it cannot execute a check-only build.

Cost of a CBGR check, by execution mode​

A reference check, bytecode dispatch, and the work performed by the program are different costs. Measure the whole workload in the selected backend before deciding whether reference-check elimination matters.

Interpreter​

The interpreter performs instruction dispatch and runtime bookkeeping. A native microbenchmark of the CBGR fast path does not predict the cost of an interpreted field access or method call.

AOT​

Tier-aware lowering can omit runtime validation for proven references. Surviving managed-reference checks still perform validation. The amount of eliminated work depends on what analysis proves for that program; it is not a fixed percentage selected by the debug or release profile.

Cross-tier transitions​

Embedding APIs may cross an interpreter/native boundary. Their calling conventions, value layouts, and reference lifetime rules must agree; do not infer a zero-cost transition from the fact that both backends consume VBC.

Memory costs across tiers​

Both backends use Verum's memory-management model, but lowering and runtime representation affect allocation and access costs. Benchmark the target platform and workload instead of treating one allocation or dereference timing as a language guarantee.

Shared vs per-tier state​

Code caches and specialisation state depend on the execution backend. The chosen API determines which runtime resources are shared. See execution environment for the runtime structures and memory model for ownership and allocation.

See also​