aot-lang 0.2.0

Ahead-of-time compile the ir-lang IR into a single linked image.
Documentation

Installation

[dependencies]
aot-lang = "0.2"
ir-lang = "1"

You build the input functions with ir-lang, so it belongs alongside aot-lang. For a no_std build, disable default features: aot-lang = { version = "0.2", default-features = false }.

How it works

A compile is two stages, one per dependency it wires:

  1. Lower — each ir_lang::Function is validated and compiled to a codegen-lang Program of register bytecode. Only well-formed SSA is ever lowered.
  2. Encode and link — each program is encoded into a .text section with a symbol at its start, and linker-lang places the objects at addresses, resolves the entry point, and produces the image.

A malformed function is rejected in the first stage as AotError::Codegen; a name collision or a missing entry point fails the second as AotError::Link.

Quick Start

Compile a single function into an image, with the function as the entry point:

use aot_lang::compile;
use ir_lang::{Builder, BinOp, Type};

// fn square(x: int) -> int { x * x }
let mut b = Builder::new("square", &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
let sq = b.bin(BinOp::Mul, x, x);
b.ret(Some(sq));

let image = compile(&b.finish()).expect("square is well-formed");

assert_eq!(image.entry(), Some(0));
assert_eq!(image.symbol("square"), Some(0));
assert!(!image.section(".text").unwrap().data().is_empty());

Place several functions in one image with the Compiler builder, choosing the base address and entry point:

use aot_lang::Compiler;
use ir_lang::{Builder, BinOp, Type};

// fn add(a: int, b: int) -> int { a + b }
let mut add = Builder::new("add", &[Type::Int, Type::Int], Type::Int);
let a = add.block_params(add.entry())[0];
let b = add.block_params(add.entry())[1];
let sum = add.bin(BinOp::Add, a, b);
add.ret(Some(sum));

// fn answer() -> int { 42 }
let mut answer = Builder::new("answer", &[], Type::Int);
let c = answer.iconst(42);
answer.ret(Some(c));

let mut compiler = Compiler::new();
compiler.base_address(0x40_0000).entry("add");
compiler.add(&add.finish()).unwrap();
compiler.add(&answer.finish()).unwrap();
let image = compiler.link().unwrap();

assert_eq!(image.entry(), Some(0x40_0000));
assert_eq!(image.symbol("add"), Some(0x40_0000));
assert!(image.symbol("answer").is_some());

API Overview

For a complete reference with examples for every item, see docs/API.md.

  • compile — ahead-of-time compile one function into an image, with it as the entry point
  • Compiler — a builder that accumulates functions and links them into one image, with a configurable base address and entry point
  • AotError — the failure type, distinguishing the code-generation stage from the link stage
  • Image — the linked result: laid-out code, a resolved symbol table, and an entry point (re-exported from linker-lang)
  • OutputSection — one laid-out section of an image (re-exported from linker-lang)

Highlights

  • Small surface — one function, one builder, one error type. Nothing to configure that the task does not need.
  • Two-stage pipeline — code generation and linking are separate, wired crates, and a failure names which stage it came from.
  • Deterministic output — the object-code encoding is fixed little-endian, so recompiling the same IR yields byte-identical images across hosts.
  • no_std-ready — needs only alloc, performs no I/O, and forbids unsafe. The whole pipeline drops the standard library together.
  • Cross-platform — Linux, macOS, and Windows on x86-64 and ARM64, verified in CI on all three operating systems and at the MSRV.

Contributing

See REPS.md for the engineering standards and the definition of done, and dev/ROADMAP.md for the plan to 1.0. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.