Skip to main content

Code Generation & Backends — From VIR to Native Code

PARTIAL(Interpreter full; Cranelift JIT/AOT, Bytecode VM, WASM, GPU/MLIR under active development)

The final compiler stage consumes the unified VIR and produces something executable. AdeshLang has several backends, each a different "final consumer":

┌──► Interpreter (tree-walk; full)

┌───────┐ ├──► Bytecode VM (--bytecode / --vm; partial)
VIR ──►│ codegen│ ├──► Baseline / Adaptive JIT (--jit; partial)
└───────┘ ├──► Native Cranelift JIT (--jit-native / --njit; partial)
├──► AOT Native (--aot; partial)
├──► WebAssembly (--wasm; partial)
└──► GPU / MLIR (--gpu; planned)

Pipeline Position

Source → Lexer → Parser → AST → HIR → MIR/Borrow → VIR ──► CodeGen ──► Executable

┌───────────┼───────────────┐
▼ ▼ ▼
Interpreter Cranelift Bytecode/WASM
(reference) (native) (portable)

VIR is the single handoff point. Every backend implements the same Backend trait: fn emit(vir: &VirModule): Result<Artifact>.


Backend Status (Honest)

BackendStatusCLI FlagSource PathBest For
Interpreter✅ Fulladesh run (default)src/execution/, src/backends/interpreter_backend/dev, debugging, REPL
Bytecode VM🟡 Partial--bytecode / --vmsrc/backends/ bytecode modules, src/execution/bytecode/scripting, portability
JIT (baseline/adaptive)🟡 Partial--jitsrc/backends/jit/adaptive/, tiered/, optimizations/long-running compute
Native Cranelift JIT🟡 Partial--jit-native / --native-jit / --njitsrc/backends/jit/cranelift/, src/backends/jit/native/compute-intensive
AOT Native🟡 Partial--aot / adesh buildsrc/backends/aot/abi.rs, cache.rs, module_linking.rs, object_gen.rs, static_linker.rs, cranelift/final binaries
WebAssembly🟡 Partial--wasmsrc/backends/wasm_backend/, src/backends/wasm/browser, edge
GPU / MLIR🔵 Planned--gpusrc/backends/mlir/CUDA/Vulkan kernels

All flags are defined in src/cli/args.rs.


Architecture Diagram — VIR to Targets

VIR Module (SSA, validated)

┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ VIR │ │ VIR │ │ VIR │
│ Lower │ │ Lower │ │ Lower │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────────┐ ┌────▼────────┐ ┌─────▼───────┐
│Interpreter │ │ Cranelift │ │ Bytecode │
│ (tree-walk) │ │ IR (CLIF) │ │ Emitter │
└────┬────────┘ └────┬────────┘ └─────┬───────┘
│ │ │
┌────▼────────┐ ┌────▼────────┐ ┌─────▼───────┐
│ Env + Value│ │ Machine Code│ │ Bytecode │
│ dispatch │ │ (x64/ARM) │ │ (portable) │
└────┬────────┘ └────┬────────┘ └─────┬───────┘
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ ▼
│ JIT (in-mem) AOT (object file → linker → binary)
│ │ │
└──────────────┴─────────┴──────────────► Runtime (Value, interner, GC-less)

The Interpreter Backend

Sources: src/execution/, src/backends/interpreter_backend/, src/execution/runtime_core/interpreter_core.rs

The tree-walk interpreter evaluates the AST/HIR directly with nested environments, a promise microtask queue, timers, and builtin dispatch (see Runtime & Value Model). It is the reference implementation — every other backend must match its observable behavior, including error messages and evaluation order.

adesh run myapp.adesh → interpreter (default)
adesh run --backend=interpreter myapp.adesh → explicit
adesh -i myapp.adesh → alias

How It Works

VIR instruction ──► interpreter dispatch (match on opcode)

┌──────────────┼──────────────┐
▼ ▼ ▼
Value ops Environment Builtin call
(add, mul) (load/store) (print, fs, net)
└──────────────┼──────────────┘

Value result (16-byte enum)
  • Value model: compact 16-byte Value enum (src/typesystem/value_optimized.rs) — inline numbers/bools, Arc<str> interned strings, Arc<Map> objects.
  • Environments: chained scopes with cached bindings for hot-path variable lookup.
  • Async: single-threaded event loop — promise microtasks and timers are cooperative.

When to Use It

  • Development, REPL, error-message fidelity, try/catch semantics, and any program that hasn't opted into native compilation.

Bytecode VM

Sources: src/backends/ bytecode emitter, src/execution/bytecode/, src/backends/lowering/

The bytecode VM compiles VIR to a compact, portable bytecode and executes it on a register or stack VM. It sits between interpreter and JIT in speed and complexity.

VIR ──► bytecode emitter ──► .abc (Adesh Bytecode) ──► VM loop (dispatch table)
(lower VIR ops to bytecode ops) fetch-decode-execute
adesh run --bytecode myapp.adesh # bytecode path
adesh run --vm myapp.adesh # alias

Characteristics:

PropertyDetail
PortabilitySingle bytecode file runs anywhere without recompilation
StartupFaster than interpreter for large programs (no tree walk)
Speed5–8× interpreter (see benchmarks below)
DebuggingSource maps back to .adesh spans for stack traces

JIT — Baseline, Adaptive, and Cranelift Native

Sources: src/backends/jit/adaptive/, array_ops/, cranelift/, native/, optimizations/, tiered/, mod.rs; src/backends/llvm/ (alternative LLVM path)

JIT lowers VIR toward native machine code at runtime.

Tiered JIT Architecture

VIR ──► Tier 0: Baseline JIT (fast compile, no opts)

│ hot threshold (loop count / call count)

Tier 1: Adaptive JIT (profile-guided, inline hot callees)


Tier 2: Cranelift JIT (full opts, SIMD, register allocation)


Native machine code (x86_64 / AArch64) in executable memory
  • backends/jit/tiered/ — tier management, hotness counters, deoptimization points.
  • backends/jit/adaptive/ — recompiles hot functions with profiling feedback.
  • backends/jit/cranelift/ — Cranelift CLIF generation, ISA selection, register allocation.
  • backends/jit/optimizations/ — JIT-specific opts (array bounds check elimination, etc.).
  • backends/jit/array_ops/ — specialized native emission for array-heavy code.

Cranelift Path (Native JIT)

VIR ──► Cranelift CLIF ──► Cranelift codegen ──► executable buffer (mmap RX)
│ │
vir_to_clif isa.emit (x64 / aarch64)
(src/backends/jit/cranelift/)

Because VIR is already SSA, mapping to Cranelift CLIF is direct — each VIR virtual register becomes a CLIF value, each Phi becomes a CLIF block param.

adesh run --jit file.adesh # baseline/adaptive JIT
adesh run --jit-native file.adesh # native Cranelift JIT
adesh run --njit file.adesh # alias for --jit-native
adesh run --native-jit file.adesh # alias

FFI from JIT Code

src/backends/common/ffi — C FFI import/export (extern "C" ..., import) is lowered to native call ABI so JIT'd code can call and be called from C.


AOT — Ahead-of-Time Native Binaries

Sources: src/backends/aot/abi.rs, cache.rs, module_linking.rs, object_gen.rs, runtime_bridge.rs, static_linker.rs, symbols.rs, cranelift/, cranelift_impl/, linker/, memory/, mod.rs

AOT compiles VIR to a standalone native binary via object file + linker.

VIR ──► Cranelift CLIF ──► object file (.o) ──► linker ──► native executable
│ │ │
aot/cranelift/ object_gen.rs static_linker.rs
aot/cranelift_impl/ (ELF/Mach-O/COFF) module_linking.rs
│ │
cache.rs (incremental) symbols.rs (symbol table)
│ │
abi.rs (calling convention)
runtime_bridge.rs (link runtime lib)
AOT FileRole
abi.rsCalling convention, argument passing, return handling
cache.rsIncremental compilation cache — skip unchanged modules
module_linking.rsLink multiple Adesh modules into one artifact
object_gen.rsEmit ELF (Linux), Mach-O (macOS), COFF (Windows)
static_linker.rsInvoke system linker (cc / ld) with correct flags
runtime_bridge.rsLink the Rust runtime (Value, interner, builtins) into the binary
symbols.rsSymbol table generation & demangling
cranelift/ / cranelift_impl/Cranelift backend specialization for AOT (vs JIT)
adesh build file.adesh -o app # AOT compile
./app # run native binary (no runtime install needed)
adesh build --release file.adesh # optimized AOT (more opts, no debug info)

AOT vs JIT:

PropertyJITAOT
Whenat runtime, on demandbefore execution, offline
Startupslower (compile on first run)instant (already native)
Peak speedsimilar (same Cranelift)similar
Distributionneeds Adesh runtime installedsingle self-contained binary
Use casedev, long-running servicesCLI tools, shipped apps

WebAssembly Target

Sources: src/backends/wasm_backend/, src/backends/wasm/

--wasm targets WebAssembly via VIR → WASM lowering. The same Adesh source can run in browsers and edge runtimes without rewriting.

VIR ──► WASM emitter ──► .wasm binary

wasm_backend/ (instruction selection, WASM types, linear memory)
adesh run --wasm file.adesh # run via WASM runtime
adesh build --wasm file.adesh -o app.wasm

Status is experimental — see Backends Overview for the current matrix.


GPU / MLIR Target

Sources: src/backends/mlir/

--gpu targets MLIR → CUDA/Vulkan. VIR operations (especially SIMD and parallel loops) are lowered to MLIR dialects and then to GPU kernels.

VIR (SIMD + parallel loops) ──► MLIR ──► PTX/SPIR-V ──► GPU execution

mlir/ (dialect lowering, kernel dispatch)

This is the most experimental path; the docs describe the pipeline, not stability guarantees.


CLI Reference — Backend Flags

FlagBackendMeaning
(none)Interpreterdefault; adesh run file.adesh
--bytecode, --vmBytecode VMemit + run bytecode
--jitBaseline/Adaptive JITtiered JIT with profiling
--jit-native, --native-jit, --njitCranelift Native JITfull native codegen via Cranelift
--aotAOTahead-of-time native binary
--wasmWASMWebAssembly emission
--gpuMLIR/GPUGPU kernel emission
--emit=hir|mir|vir|cfg(debug)dump IR at that stage and exit
--backend=interpreterInterpreterexplicit selection

All flags are parsed in src/cli/args.rs.


Performance Profile

Benchmarks from Backend Benchmarks (relative to interpreter = 1×):

BackendRelative SpeedStartupBest For
Interpreterinstantdev, debugging, REPL, correctness
Bytecode VM5–8×fastscripting, portability, embedded
Baseline JIT10–20×moderatelong-running compute, warm loops
Native JIT (Cranelift)100–232×slower (compile)compute-intensive, SIMD, numeric
AOT100–250×instant (precompiled)shipped binaries, CLIs, production
WASM3–6× (in browser)fastweb, edge, sandbox
GPU/MLIR500×+ (kernel-bound)slow (kernel compile)vector/parallel workloads

Numbers are workload-dependent. The interpreter is the correctness baseline — optimized backends must match its observable semantics before they are considered complete.


Lowering Example — End to End

Source:

fn square(x: i32): i32 { return x * x; }
let y = square(7);
1. VIR (SSA):
func @square(%0: i32) -> i32 { %1 = mul %0, %0; ret %1 }
func @main { %2 = call @square(7); store %2 -> y; ret }

2. Interpreter: tree-walk call frame → Value::Number(49)

3. Cranelift CLIF:
function %square(i32) -> i32 {
block0(v0: i32):
v1 = imul v0, v0
return v1
}

4. Machine code (x64):
imul edi, edi
mov eax, edi
ret

5. Bytecode:
LOAD_CONST 7
CALL @square, 1
STORE y

6. WASM:
(func $square (param i32) (result i32)
local.get 0
local.get 0
i32.mul)

Where This Fits