Axiom
A small expression-oriented language with a from-scratch compiler in C++23 on LLVM. Early development: the front end works; the JIT is next.
- Status
- Early development · v0.1.0
- Role
- Solo project
- Stack
- C++23 · LLVM 22 · CMake
- Links
- Source · Language reference
Problem #
I wanted to understand compilers by building one end to end. That meant writing my own lexer, parser and code generator, then handing the result to LLVM. I also wanted to see how far modern C++ goes when you take it seriously: no exceptions, no compiler extensions, and C++23 features where they actually help.
The target is an interactive language. You type an expression into a REPL, Axiom compiles it to native code in memory with LLVM’s ORC JIT, and it runs immediately.
Approach #
The language is deliberately small, so the effort goes into the compiler rather than the feature list:
extern sin(x) # provided by the host (libm)
def square(x) x * x # bodies are single expressions
square(4) + sin(0)
Every value is a 64-bit float. Function bodies are single expressions. The binary operators
are <, +, - and *.
Architecture #
source text
│
▼
Lexer zero-copy std::string_view tokens
│
▼
Parser precedence climbing ─> AST
│
▼
CodeGenerator llvm::IRBuilder, constant folding ─> LLVM IR
│
▼
ORC JIT planned
Each stage is its own static library (axiom_support, axiom_lexer, axiom_parser,
axiom_codegen), and every target builds under a strict warning set (-Wall -Wextra -Wpedantic -Wshadow -Wconversion …).
- Lexer: tokens are non-owning
std::string_viewslices of the source buffer, each tagged with a 1-based line and column. The lexer makes no heap allocations. - Parser: operator-precedence climbing driven by a table, with single-token lookahead and no backtracking. Operators are left-associative.
- Diagnostics without exceptions: every parse function returns
Expected<T> = std::expected<T, CompilerError>. An error carries a message and a source location and prints asaxiom:LINE:COL: error: MESSAGE. - Code generation: numbers, variables, binary operators and calls lower to IR through
llvm::IRBuilder. Constants fold as the IR is built, so10 + 5 * 2becomes20.0.
Trade-offs #
std::expectedover exceptions. Error paths are visible in every signature, and the happy path costs nothing extra. The price is more explicit plumbing in the parser.- Zero-copy tokens. This is fast and allocation-free, but the source buffer must outlive every token and AST node that points into it. The architecture notes write down that ownership rule.
- One type (double). It keeps codegen simple while the pipeline is being built. A type system comes after the JIT works, not before.
Status and next steps #
Done: lexer, parser, and IR generation for expressions.
Next, in order:
- IR for prototypes and function definitions
- Native execution with ORC JIT (
LLJIT) - Resolving
externfunctions against host symbols - An interactive REPL, where each top-level expression gets its own
ResourceTrackerand is freed after it runs if/then/elselowered to basic blocks joined by a PHI node- Unit tests
Until the REPL lands, the axiom binary only runs a smoke test of the code generator.