Axe is a compiled programming language with a focus on type safety, ease of concurrency, and performance.
It provides a clean syntax for systems programming with modern and parallel language features.
use std.io;
use std.string;
model Person {
name: string;
age: i32;
}
def main() {
parallel for mut i = 0 to 10 {
val person = Person{name: "Alice", age: i};
println person.age;
}
}
- Latest release of the Axe compiler
- Documentation on the language
- The Visual Studio Code extension for Axe
- The LSP, namely Axels, installation involves just downloading it and adding it to PATH.
- Saw, a build tool and package manager for Axe
- Type Safety: Safe by default
- Parallelism at the core of the language Supports parallelism at the language level, making it easy to write programs that can take advantage of multiple CPU cores.
- Clean Syntax: Intuitive syntax inspired by modern languages
- Standard Library: Built-in support for numerous data structures and utilities
- Cross-platform: Works on Windows, macOS, and Linux
- Fast Compilation: Optimized build system for quick iteration
- Functions and variables, immutability by default
- Control flow (if/else, for loops,
loopconstruct) - Pointers and memory management with high level abstractions
- Parallel processing support at the core of the language
- Built-in println for debugging
Without already having an axe binary, clone https://github.com/axelang/axe-bootstrap.git to get axe latest on POSIX systems.
Bootstrapping process:
git clone https://github.com/axelang/axe-bootstrap.git
cd axe-bootstrap
chmod +x install.sh
sudo ./install.shIf you already have an axe binary (Windows users can download the .zip from GitHub releases), the build process is simply:
git clone https://github.com/axelang/axe.git
cd axe/source/compiler
axe axc -o axe --releaseOr use saw build --release with the saw build tool.
This will create the axe executable.
# Compile and run a program
./axe hello.axe -r
# Compile to executable
./axe hello.axe
# Compile for release (optimized)
./axe hello.axe --release -r
# Compile to shared library
./axe mylib.axe -dlluse std.string;
def greet(name: string): void {
println $"Hello, {name}";
}
def main() {
greet(str("Axe"));
}
Axe supports tagged unions, allowing a value to take one of several typed forms. Each variant has its own fields, and the active variant is determined by the tag:
model Expr {
tag: string;
data: union {
literal: model { value: i32 };
variable: model { name: string };
binary: model { op: string; left: Expr; right: Expr };
};
}
def main() {
val x = Expr{
tag: "literal",
data: literal{ value: 42 }
};
println x.data.literal.value; // 42
}
Tagged unions provide a safe and expressive way to model AST nodes, protocol messages, and other variant-based structures.
Generics in Axe allow writing of functions and models that operate on different types while maintaining type safety. You can specify type parameters using square brackets [T], and use type-specific logic with when clauses.
use std.io;
use std.string;
model SomeModel {
pub def some_function[T](arg: T): T {
when T is i32 {
return arg + 1;
}
when T is f32 {
return arg * 2.0;
}
when T is string {
return concat_c(arg, "!\n");
}
}
}
def main() {
println(SomeModel.some_function[i32](5));
println(SomeModel.some_function[f32](5.0));
println(SomeModel.some_function[string](str("Hello")));
println(SomeModel.some_function(3.0));
}
use std.io;
/// Convert 2D coordinate (x, y) into 1D index
def idx(x: i32, y: i32, width: i32): i32 {
return y * width + x;
}
/// Print the grid (1-D list)
def print_grid(grid: ref list(i32), width: i32, height: i32) {
for mut y = 0; y < height; y++ {
for mut x = 0; x < width; x++ {
if grid.data[idx(x, y, width)] == 1 {
print "■";
} else {
print "□";
}
}
println "";
}
}
/// Count live neighbors around (x, y)
def count_neighbors(grid: ref list(i32), x: i32, y: i32, width: i32, height: i32): i32 {
mut count: i32 = 0;
for mut dy = -1; dy <= 1; dy++ {
for mut dx = -1; dx <= 1; dx++ {
if dx == 0 and dy == 0 {
continue;
}
val nx = x + dx;
val ny = y + dy;
if nx >= 0 and nx < width and ny >= 0 and ny < height {
if grid.data[idx(nx, ny, width)] == 1 {
count++;
}
}
}
}
return count;
}
/// Compute next generation
def next_generation(grid: ref list(i32), new_grid: ref list(i32), width: i32, height: i32) {
for mut y = 0; y < height; y++ {
for mut x = 0; x < width; x++ {
val i = idx(x, y, width);
val neighbors = count_neighbors(grid, x, y, width, height);
if grid.data[i] == 1 {
if neighbors == 2 or neighbors == 3 {
new_grid.data[i] = 1;
} else {
new_grid.data[i] = 0;
}
} else {
if neighbors == 3 {
new_grid.data[i] = 1;
} else {
new_grid.data[i] = 0;
}
}
}
}
}
/// Copy new_grid back into grid
def copy_grid(src: ref list(i32), dst: ref list(i32), size: i32) {
for mut i = 0; i < size; i++ {
dst.data[i] = src.data[i];
}
}
def main() {
val width: i32 = 20;
val height: i32 = 20;
val size: i32 = width * height;
mut grid: list(i32);
mut new_grid: list(i32);
for mut i = 0; i < size; i++ {
append(grid, 0);
append(new_grid, 0);
}
grid.data[idx(2,1,width)] = 1;
grid.data[idx(3,2,width)] = 1;
grid.data[idx(1,3,width)] = 1;
grid.data[idx(2,3,width)] = 1;
grid.data[idx(3,3,width)] = 1;
println "Conway's Game of Life\n";
for mut gen = 0; gen < 20; gen++ {
print "Generation ";
println gen;
println "";
print_grid(addr(grid), width, height);
next_generation(addr(grid), addr(new_grid), width, height);
copy_grid(addr(new_grid), addr(grid), size);
println "\n---\n";
}
}
- Control flow constructs, functions, variables, fundamentals...
- Immutability by default
- Parallel for
- Union types
- Pure
parallel { ... }blocks - Syntax (
single { ... }) for isolating single threaded behaviours in parallel contexts - Map and reduce clauses
- Smart type inference based on RHS of exprs.