Docs / Architecture

14. AOT Compilation Pipeline

The Ahead-of-Time (AOT) compiler transforms Dryad bytecode into native machine code through a multi-stage pipeline that avoids emitting assembly or object files directly — instead it leverages LLVM IR as its backend target. The pipeline: (1) Bytecode → Micro-IR — stack-based opcodes are converted to a register-based 3-address SSA form where each instruction is R1 = R2 op R3, using the Metadata Header to drive type mapping; (2) Micro-IR → LLVM IR — each Micro-IR instruction maps to LLVM IR instructions with native types (double, i32, i8*); (3) LLVM Optimization — LLVM's pass manager applies constant folding, dead code elimination, loop unrolling, SIMD vectorization, and inlining at -O2; (4) Code Generation — LLVM's backend emits object files for ARM64 or x86_64; (5) Linking — the object file is linked against the Dryad runtime library to produce an ELF or PE executable. Every intrinsic syscall has dual entry points: a Fast Path (native C ABI, zero overhead) and a Slow Path (Variant-based, for dynamic fallback). The dryad --analyze tool reports every call site that would use the Slow Path, with suggestions for promoting to Fast Path via type annotations.

Pipeline StageInputOutputKey Operation
Bytecode DecodeType-Annotated OpCodesMicro-IR (3-address SSA)Stack-to-register conversion using Metadata Header
IR EmissionMicro-IRLLVM IR (.ll)Type mapping: f64→double, i32→i32, string→i8*, object→i8*
LLVM OptimizationLLVM IROptimized LLVM IRConstant folding, DCE, loop unrolling, SIMD, inlining (-O2)
Code GenerationOptimized IRObject file (.o)LLVM backend: instruction selection, register allocation
LinkingObject file(s)ELF / PE executableLink against Dryad runtime (intrinsics + RC GC)
Binary OutputLinked executableNative binaryReady for deployment — no VM or runtime DLL required
Full Compilation Pipeline with LLVM
 1 // Dryad Source: function add(a, b) { return a + b; }
 2 //     ↓  (interpreter collects types: a=f64, b=f64)
 3 // AST + Type Observations
 4 //     ↓  (bytecode compiler emits type-specialized opcodes)
 5 // Type-Annotated Bytecode:
 6 //   Metadata Header: a->f64(OBSERVED), b->f64(OBSERVED)
 7 //   Opcodes: LOAD_FAST a, LOAD_FAST b, ADD_F64, RETURN
 8 //     ↓  (stack-to-register conversion)
 9 // Micro-IR (3-address SSA):
10 //   %1 = load_f64 @a
11 //   %2 = load_f64 @b
12 //   %3 = fadd_f64 %1, %2
13 //   ret_f64 %3
14 //     ↓  (LLVM IR emission)
15 // LLVM IR (.ll):
16 //   define double @add_f64(double %a, double %b) {
17 //     %result = fadd double %a, %b
18 //     ret double %result
19 //   }
20 //     ↓  (LLVM optimization + codegen)
21 // ARM64 Assembly:
22 //   fadd d0, d0, d1
23 //   ret
24 //     ↓  (link against runtime)
25 // Native Binary (ELF or PE)

The entire pipeline is type-driven. Because the Metadata Header recorded a and b as f64, every layer emits native floating-point operations. The Bind Once decision at the bytecode level propagates all the way to the generated binary.

Fast Path / Slow Path Intrinsic Dispatch
 1 // Every intrinsic syscall has two entry points in the C++ runtime.
 2 // Both share the same internal implementation — only the
 3 // argument passing convention differs.
 4 
 5 // C++ Runtime (internal logic, shared):
 6 //   int64_t internal_read(int32_t fd, void* buf, size_t len) {
 7 //     return read(fd, buf, len);  // the actual syscall
 8 //   }
 9 
10 // Fast Path (used by AOT when types are known):
11 //   extern "C" int64_t syscall_read_fast(
12 //     int32_t fd, void* buf, size_t len
13 //   ) {
14 //     return internal_read(fd, buf, len);
15 //   }
16 //   // Arguments passed in CPU registers (rdi, rsi, rdx on x86_64)
17 //   // No Variant construction — zero overhead
18 
19 // Slow Path (used by VM and AOT fallback):
20 //   Variant syscall_read_variant(Variant* args) {
21 //     int32_t fd = args[0].as_i32();
22 //     // ... extract, validate, box/unbox ...
23 //     return Variant::from_i64(internal_read(fd, buf, len));
24 //   }
25 
26 // AOT Compiler decision logic:
27 //   if (all_arguments_have_static_types) {
28 //     emit("call i64 @syscall_read_fast(i32, i8*, i64)");
29 //   } else {
30 //     emit("call %Variant @syscall_read_variant(%Variant*)");
31 //   }

The dual-port design ensures zero overhead for statically-typed code paths while preserving correctness for dynamic code. The AOT compiler selects the appropriate entry point per call site based on the Metadata Header.

dryad --analyze — Slow Path Detection
 1 $ dryad --analyze server.dryad
 2 
 3 Dryad AOT Analysis Report
 4 ━━━━━━━━━━━━━━━━━━━━━━━
 5 File: server.dryad
 6 
 7 [SLOW PATH] line 45 col 12 — processRequest(data, callback)
 8   Reason: parameter 'data' type UNKNOWN
 9   Impact: ~120 Variant operations per invocation
10   Fix: add type hint 'let data: Buffer'
11 
12 [SLOW PATH] line 78 col 8 — data + transform
13   Reason: operator '+' receives merged type 'string | number'
14   Impact: ~8 Variant operations per invocation
15   Fix: separate numeric and string paths
16 
17 [FAST PATH] 142 of 150 call sites (94.7%)
18 
19 Summary:
20   Slow Path call sites: 2
21   Variant ops per request: ~128
22   Recommended annotations: 1

The analyzer shows exactly where dynamic types degrade performance. Each warning includes a concrete fix suggestion, aligning with Dryad's philosophy of progressive performance — add types only where they matter.

Found an error in the spec? Open a PR.
Official Version: 1.0 · May 2026