Skip to main content
The Leo compiler is a multi-stage compiler that transforms Leo source code into Aleo bytecode executable on the Aleo blockchain. This page provides an in-depth look at the compiler’s architecture, crate organization, and overall design.

Compilation Pipeline

Leo’s compilation process follows a well-defined pipeline:

Pipeline Stages

  1. Lexical Analysis: The leo-parser-rowan crate uses the logos library to tokenize source code
  2. Parsing: A Rowan-based parser constructs an untyped syntax tree from tokens
  3. AST Construction: The leo-parser crate converts the Rowan parse tree into a typed Abstract Syntax Tree
  4. Compiler Passes: The leo-passes crate applies approximately 25 sequential transformations
  5. Code Generation: The final pass generates Aleo bytecode instructions
The Leo compiler uses a red-green tree approach with Rowan, enabling incremental parsing and better error recovery than traditional parser generators.

Crate Architecture

The Leo compiler is organized into a set of interdependent crates, each with a specific responsibility:

Foundation Crates

leo-span

Provides source location tracking for error reporting.
Dependencies: fxhash, indexmap, serde Key Features:
  • Fast hash-based span lookups
  • Deterministic ordering with IndexMap
  • Serializable for AST snapshots

leo-errors

Centralized error handling for all compiler stages. Error Code Format: E{PREFIX}037{CODE}
  • EPAR037XXXX: Parser errors (0-999)
  • EAST037XXXX: AST errors (2000-2999)
  • ECMP037XXXX: Compiler errors (6000-6999)
Security: Errors must never leak internal implementation details. All error messages are carefully crafted to be informative without exposing compiler internals.

AST and Parsing

leo-ast

Defines all Abstract Syntax Tree node types. Core Requirements:
  • Every node implements the Node trait (using simple_node_impl! macro)
  • Every node contains Span and NodeID for error reporting and traversal
  • Uses IndexMap for deterministic ordering (never HashMap)
  • Large enum variants must be boxed to control memory layout

leo-parser-rowan

Lexer and untyped parser built on the Rowan library. Architecture:
  • Grammar defined in grammar.rs
  • Tokenization via logos crate
  • Produces a lossless syntax tree (includes whitespace and comments)
  • Error recovery built into the parser

leo-parser

Converts the Rowan parse tree into typed AST nodes. Responsibilities:
  • Type-safe AST construction
  • Initial semantic validation
  • Span preservation from source to AST
Testing:

Compiler Passes

leo-passes

Implements all compiler transformations and optimizations. Pass Trait:
All passes are executed sequentially through the CompilerState in leo-compiler/src/compiler.rs:186-247:
Pass ordering is critical. Each pass depends on invariants established by previous passes. For example, SSA form must be established before flattening.

leo-compiler

Orchestrates parsing and all compiler passes. Compiler Structure (leo-compiler/src/compiler.rs:58-73):
Compiler State (leo-passes/src/pass.rs:26-52):

Supporting Crates

leo-abi / leo-abi-types

Generate Application Binary Interface definitions. Generated After Monomorphization: ABIs are captured immediately after the monomorphization pass to ensure all const generic types are resolved (leo-compiler/src/compiler.rs:213-215).

leo-fmt

Leo source code formatter (uses leo-parser-rowan).

leo-disassembler

Converts Aleo bytecode back to human-readable format.

leo-package

Parses and manages Leo project structure (program.json, etc.).

leo-test-framework

Test harness for .leo test files. Test Structure:
  • Tests in tests/tests/{category}/
  • Expectations in tests/expectations/{category}/
  • Use UPDATE_EXPECT=1 to regenerate expectations

Data Flow Through Compiler

1. Source to AST

2. Pass Execution

Each pass is wrapped in do_pass which handles AST snapshots:

3. Code Generation

The final pass generates Aleo bytecode (leo-compiler/src/compiler.rs:300):

Memory and Performance

Design Principles

  1. Pre-allocation: Use with_capacity when final size is known
  2. Avoid Cloning: Prefer references and into_iter() over .clone() and iter().cloned()
  3. Iterator Chains: Avoid intermediate vectors and unnecessary .collect()
  4. Deterministic Ordering: Always use IndexMap or IndexSet, never HashMap or HashSet

Hot Path Optimizations

The compiler applies several optimizations in performance-critical paths:
  • Symbol Interning: Identifiers are interned to reduce string allocation
  • Arena Allocation: Node IDs reference arena-allocated nodes
  • Copy-on-Write: AST nodes are modified in-place when possible
Every unwrap() in the codebase must be justified with a comment explaining why it’s safe. In production paths, always use proper error handling.

Security Guarantees

Unsafe Code Prohibition

The following crates forbid unsafe code:
  • leo-span
  • leo-passes
  • leo-compiler
  • leo-errors
  • leo-package
This is enforced with #![forbid(unsafe_code)] at the crate root.

Input Validation

  • All external input is validated at trust boundaries
  • Parser rejects malformed syntax with descriptive errors
  • Type checker enforces type safety
  • Bounds checking on all array accesses

Fail-Closed Design

The compiler follows a fail-closed approach: when uncertain, reject the program rather than making assumptions.

Debugging and Testing

AST Snapshots

Enable AST snapshots to see transformations:
This generates:
  • program_name.initial.json - AST after parsing
  • program_name.TypeChecking.json - AST after type checking
  • program_name.Flattening.json - AST after flattening
  • etc.

Running Tests