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 Stage | Input | Output | Key Operation |
|---|---|---|---|
| Bytecode Decode | Type-Annotated OpCodes | Micro-IR (3-address SSA) | Stack-to-register conversion using Metadata Header |
| IR Emission | Micro-IR | LLVM IR (.ll) | Type mapping: f64→double, i32→i32, string→i8*, object→i8* |
| LLVM Optimization | LLVM IR | Optimized LLVM IR | Constant folding, DCE, loop unrolling, SIMD, inlining (-O2) |
| Code Generation | Optimized IR | Object file (.o) | LLVM backend: instruction selection, register allocation |
| Linking | Object file(s) | ELF / PE executable | Link against Dryad runtime (intrinsics + RC GC) |
| Binary Output | Linked executable | Native binary | Ready for deployment — no VM or runtime DLL required |
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.
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.
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