Skip to content

Ai Guide

The complete reference for AI agents writing AXON programs.

After reading this document, you will be able to write any AXON program. This is the only document you need.


  1. What is AXON?
  2. Source Format
  3. Program Structure
  4. Type Reference
  5. Expression Reference
  6. Operator Reference
  7. Declaration Reference
  8. Module System & Imports
  9. Complete Examples
  10. Cookbook: Common Patterns
  11. Compilation & Execution
  12. Anti-Patterns: What NOT To Do
  13. Performance Tips
  14. Memory Layout
  15. QBE IL Patterns
  16. Debugging Guide
  17. Quick Reference Card
  18. Cross-Platform GUI

AXON is a compiled, statically-typed programming language that uses S-expression syntax (fully parenthesized prefix notation). It compiles to native code via QBE or LLVM backends.

Critical rules:

  • Every literal must have a type annotation. Write (i64 42), never just 42.
  • Every expression has a known type. There are no implicit conversions.
  • All type conversions are explicit. Use (cast target_type expr).
  • Variables are mutable. Introduced by let, updated by set.
  • Functions are called with call. Write (call func_name args...).
  • The entry point is main. It must return i32.

AXON source files use the .axs extension and contain S-expressions.

Everything is a parenthesized list in prefix notation:

(operator operand1 operand2)

Examples:

(add x y) ;; x + y
(mul (i64 3) (i64 7)) ;; 3 * 7
(call fib (i64 10)) ;; fib(10)

Line comments start with ;; and extend to end of line:

;; This is a comment
(i64 42) ;; inline comment

Any amount of whitespace (spaces, tabs, newlines) between tokens. Indentation is not significant.


Every AXON program is a module containing declarations:

(module
;; declarations go here
)

A valid executable module must contain a main function that returns i32:

(module
(fn main () i32
(i32 0)))

Declaration order does not matter. Functions can call functions declared later in the file.


TypeSizeRange / Description
void0No value
bool1 bytetrue or false
i81 byte-128 to 127
i162 bytes-32,768 to 32,767
i324 bytes-2,147,483,648 to 2,147,483,647
i648 bytes-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
u81 byte0 to 255
u162 bytes0 to 65,535
u324 bytes0 to 4,294,967,295
u648 bytes0 to 18,446,744,073,709,551,615
f324 bytes32-bit IEEE 754 floating point
f648 bytes64-bit IEEE 754 floating point
TypeSyntaxExample
Pointer(ptr T)(ptr i8) — pointer to byte
Array(arr T N)(arr i32 10) — 10-element i32 array
Slice(slice T)(slice u8) — byte slice (ptr + len)
StructNamePoint — user-defined struct type

Every literal value must be wrapped in a type annotation:

(i64 42) ;; integer 42 as i64
(i32 -7) ;; integer -7 as i32
(u8 255) ;; integer 255 as u8
(f64 3.14) ;; float 3.14 as f64
(f32 1.0) ;; float 1.0 as f32
(bool true) ;; boolean true
(bool false) ;; boolean false
(str "hello") ;; string literal (type: (ptr i8))

You must NEVER write a bare literal. 42 by itself is a variable reference to something named 42, which will fail.


(i64 42) ;; 64-bit signed integer
(i32 0) ;; 32-bit signed integer
(u8 0xFF) ;; 8-bit unsigned integer (hex)
(u32 0b10101010) ;; 32-bit unsigned integer (binary)
(f64 3.14159) ;; 64-bit float
(f32 2.718) ;; 32-bit float
(f64 1e-10) ;; scientific notation
(bool true) ;; boolean
(str "Hello, World!") ;; string (type: (ptr i8))

Bare identifiers reference variables in scope:

x ;; reference variable x
counter ;; reference variable counter
my_value ;; reference variable my_value

A variable must be defined before it can be referenced — either as a function parameter or via a let binding.

(add a b) ;; a + b (integer or float)
(sub a b) ;; a - b (integer or float)
(mul a b) ;; a * b (integer or float)
(div a b) ;; a / b (integer or float)
(mod a b) ;; a % b (integer only)
(eq a b) ;; a == b → bool
(ne a b) ;; a != b → bool
(lt a b) ;; a < b → bool
(le a b) ;; a <= b → bool
(gt a b) ;; a > b → bool
(ge a b) ;; a >= b → bool
(band a b) ;; a & b (bitwise AND, integer only)
(bor a b) ;; a | b (bitwise OR, integer only)
(bxor a b) ;; a ^ b (bitwise XOR, integer only)
(shl a b) ;; a << b (shift left, integer only)
(shr a b) ;; a >> b (shift right, integer only)
(land a b) ;; a && b (logical AND, short-circuit, bool only)
(lor a b) ;; a || b (logical OR, short-circuit, bool only)

Both operands must have the same type. The result type matches the operand type, except for comparison operators which always return bool.

(neg x) ;; -x (arithmetic negation, integer or float)
(not x) ;; !x (logical NOT, bool → bool)
(bnot x) ;; ~x (bitwise NOT, integer only)
(call function_name arg1 arg2 ...)

Examples:

(call fib (i64 35)) ;; one argument
(call add (i64 3) (i64 7)) ;; two arguments
(call print_i64 x) ;; one variable argument
(call print_str (str "hello")) ;; string argument
(call main) ;; no arguments

Argument types must match the function’s parameter types exactly.

;; if-then-else (produces a value)
(if condition then_expr else_expr)
;; if-then only (void, no value)
(if condition then_expr)

The condition must be bool. When both branches are present, they must have the same type.

Examples:

;; Returns the larger of a and b
(if (gt a b) a b)
;; Conditional with complex expressions
(if (lt n (i64 2))
n
(add (call fib (sub n (i64 1)))
(call fib (sub n (i64 2)))))
;; Void if (no else branch)
(if (gt x (i64 0))
(call print_i64 x))
(while condition body)

The condition must be bool. The body is evaluated repeatedly while the condition is true. While loops have type void.

(while (gt n (i64 0))
(block
(call print_i64 n)
(set n (sub n (i64 1)))))
;; Without type annotation (type inferred from initializer)
(let (name init_expr) body_expr)
;; With type annotation
(let (name type init_expr) body_expr)

let introduces a new mutable variable. The variable is visible in the body expression. The value of the let expression is the value of the body.

;; Introduce x = 42, then compute x + 1
(let (x (i64 42))
(add x (i64 1)))
;; With explicit type
(let (x i64 (i64 42))
(add x (i64 1)))
;; Nested let bindings
(let (a (i64 10))
(let (b (i64 20))
(add a b)))
(set variable_name new_value)

Updates a previously-introduced variable. The new value must have the same type.

(set x (i64 5))
(set counter (add counter (i64 1)))
(set result (mul result n))
(block expr1 expr2 ... exprN)

Evaluates expressions in sequence. The value of the block is the value of the last expression. All preceding expressions are evaluated for side effects.

(block
(call print_i64 (i64 1))
(call print_i64 (i64 2))
(i64 42)) ;; block value is 42
(ret expr) ;; return a value
(ret) ;; return void

Returns from the enclosing function. The return value must match the function’s return type.

(fn main () i32
(ret (i32 0)))
(cast target_type expr)

Converts a value from one type to another. Only certain casts are valid:

(cast i64 (i32 42)) ;; i32 → i64 (sign-extend)
(cast i32 (i64 100000)) ;; i64 → i32 (truncate)
(cast f64 (i64 42)) ;; i64 → f64 (int-to-float)
(cast i64 (f64 3.14)) ;; f64 → i64 (float-to-int, truncates)
(cast u64 (i64 -1)) ;; i64 → u64 (reinterpret sign)
(cast f64 (f32 1.0)) ;; f32 → f64 (widen)
;; Array indexing
(idx my_array (i64 3))
;; Struct field access
(fld my_point x)
;; Address-of
(addr my_var)
;; Dereference pointer
(deref my_ptr)
;; Array literal
(arr.new i64 (i64 1) (i64 2) (i64 3))
;; Struct literal
(new Point (x (f64 1.0)) (y (f64 2.0)))
(defer expr)

Registers expr to run when the enclosing function exits. Use defer to guarantee cleanup regardless of how the function returns.

;; Acquire a resource, defer its release
(fn read_file ((path (ptr i8))) i64
(let (fd (call open_file path))
(block
(defer (call close_file fd)) ;; guaranteed to run at function exit
(call process_data fd)))) ;; even if process_data fails via try

Multiple defers execute in last-in, first-out order (like Go’s defer). The most recently registered defer runs first:

;; Output: 1, 2, 3
(fn lifo_demo () void
(block
(defer (call print_i64 (i64 3))) ;; registered first → runs last
(defer (call print_i64 (i64 2))) ;; registered second → runs second
(defer (call print_i64 (i64 1)))) ;; registered third → runs first

This is intuitive for resource cleanup — resources acquired later are released first:

(fn multi_resource () void
(let (db (call open_db))
(block
(defer (call close_db db)) ;; closes second
(let (tx (call begin_transaction db))
(block
(defer (call end_transaction tx)) ;; closes first
(call do_work tx))))))

Deferred expressions run at every function exit point:

  • (ret ...) — return value is evaluated first, then defers run, then return
  • Implicit return (end of function body) — defers run after the last expression
  • (try ...) error propagation — defers run before the error propagates
;; Defer runs on both early return and normal exit
(fn safe_compute ((x i64)) i64
(block
(defer (call cleanup))
(if (lt x (i64 0))
(ret (i64 -1))) ;; cleanup runs before returning -1
(mul x x))) ;; cleanup also runs on normal exit
;; Defer with try/result — cleanup on error path
(fn read_file ((path (ptr i8))) (result i64 i64)
(block
(let (fd (try (call open_file path))) ;; if open fails, no defer yet
(block
(defer (call close_file fd)) ;; registered after successful open
(try (call read_data fd)))))) ;; if read fails, close_file still runs
  • defer has type void.
  • Defer is per-function scope — a defer inside a block, if, or while still runs at function exit, not block exit.
  • The deferred expression is not evaluated when defer is encountered — it is saved and runs later.
  • Deferred expressions can reference variables that were in scope at the defer site.

OperatorMeaningOperand TypesResult TypeExample
addAdditioninteger, floatsame(add x y)
subSubtractioninteger, floatsame(sub x (i64 1))
mulMultiplicationinteger, floatsame(mul a b)
divDivisioninteger, floatsame(div total count)
modRemainderintegersame(mod n (i64 2))
eqEqualany comparablebool(eq x (i64 0))
neNot equalany comparablebool(ne x y)
ltLess thaninteger, floatbool(lt n (i64 2))
leLess or equalinteger, floatbool(le x max)
gtGreater thaninteger, floatbool(gt a b)
geGreater or equalinteger, floatbool(ge len (i64 0))
bandBitwise ANDintegersame(band flags mask)
borBitwise ORintegersame(bor a b)
bxorBitwise XORintegersame(bxor x y)
shlShift leftintegersame(shl x (i64 3))
shrShift rightintegersame(shr x (i64 1))
landLogical ANDboolbool(land cond1 cond2)
lorLogical ORboolbool(lor a b)
OperatorMeaningOperand TypeResult TypeExample
negNegateinteger, floatsame(neg x)
notLogical NOTboolbool(not done)
bnotBitwise NOTintegersame(bnot mask)

(fn name ((param1 type1) (param2 type2) ...) return_type
body_expression)

Every function has:

  • A unique name
  • Zero or more typed parameters
  • A return type
  • A single body expression (use block for multiple statements)
;; No parameters
(fn main () i32
(i32 0))
;; One parameter
(fn square ((x i64)) i64
(mul x x))
;; Multiple parameters
(fn add ((a i64) (b i64)) i64
(add a b))
;; Void return type
(fn print_greeting () void
(call print_str (str "Hello!")))
;; Complex body with block
(fn compute ((n i64)) i64
(block
(call print_str (str "Computing..."))
(let (result (mul n n))
(block
(call print_i64 result)
result))))

Import external C functions:

(extern name ((param1 type1) ...) return_type)

For variadic functions, add ... after the last parameter:

;; Import C's puts
(extern puts ((s (ptr i8))) i32)
;; Import C's printf (variadic)
(extern printf ((fmt (ptr i8)) ...) i32)
;; Import C's exit
(extern exit ((code i32)) void)
(struct name ((field1 type1) (field2 type2) ...))
(struct Point ((x f64) (y f64)))
(struct Rect ((origin Point) (width f64) (height f64)))
(struct Node ((value i64) (next (ptr Node))))

Define compile-time constants:

(const name type value_expr)

Constants are evaluated at compile time and can be referenced anywhere in the module:

;; Numeric constants
(const PI f64 (f64 3.14159265358979))
(const MAX_SIZE u32 (u32 1024))
(const ZERO i64 (i64 0))
;; Usage in functions
(fn circle_area ((r f64)) f64
(mul PI (mul r r)))

AXON supports splitting programs across multiple .axs files using imports. There are two import forms: namespaced import and selective import.

8.1 Namespaced Import — (import alias "path")

Section titled “8.1 Namespaced Import — (import alias "path")”

Import an entire module under a namespace alias. Access its functions with alias.function_name:

;; main.axs
(module
(import math "math.axs")
(fn main () i32
(block
(call print_i64 (call math.square (i64 5))) ;; prints 25
(call print_i64 (call math.cube (i64 3))) ;; prints 27
(i32 0))))
;; math.axs — the imported library module
(module
(pub (fn square ((x i64)) i64
(mul x x)))
(pub (fn cube ((x i64)) i64
(mul x (mul x x)))))

Key points:

  • The alias (math) is any valid identifier you choose.
  • The string ("math.axs") is the file path, relative to the importing file.
  • Use dot notation to call: (call math.square ...).

8.2 Selective Import — (use "path" (names...))

Section titled “8.2 Selective Import — (use "path" (names...))”

Import specific functions directly into scope — no qualifier needed:

;; main.axs
(module
(use "utils.axs" (helper_fn format_value))
(fn main () i32
(block
;; Call directly — no namespace prefix
(call print_i64 (call helper_fn (i64 42)))
(call print_i64 (call format_value (i64 100)))
(i32 0))))
;; utils.axs
(module
(pub (fn helper_fn ((x i64)) i64
(add x (i64 1))))
(pub (fn format_value ((x i64)) i64
(mul x (i64 10)))))

8.3 Making Functions Public — (pub (fn ...))

Section titled “8.3 Making Functions Public — (pub (fn ...))”

By default, all functions are private. Wrap a function declaration in (pub ...) to make it accessible from other modules:

(module
;; PUBLIC — other modules can import and call this
(pub (fn add ((a i64) (b i64)) i64
(add a b)))
;; PRIVATE — only callable within this file
(fn internal_helper ((x i64)) i64
(mul x x)))

If you try to call a private function from another module, you get a compile error:

error: function 'internal_helper' is private in module 'mylib'

Import paths are resolved relative to the importing file’s directory:

project/
├── main.axs ;; (import math "lib/math.axs")
└── lib/
├── math.axs ;; (import utils "utils.axs") → resolves to lib/utils.axs
└── utils.axs

The compiler automatically loads transitive dependencies. If a.axs imports b.axs, and b.axs imports c.axs, all three are loaded. Circular imports are detected and handled.

You can also compile multiple files without import/use by listing them on the command line:

Terminal window
axonc a.axs b.axs -o program

In this mode, all files share a flat namespace — every function is visible to every file. No pub wrapper is needed. This mode is independent of the import system.

Here is a full multi-file program:

math.axs:

(module
(pub (fn square ((x i64)) i64
(mul x x)))
(pub (fn sum_to ((n i64)) i64
(let (total (i64 0))
(let (i (i64 1))
(block
(while (le i n)
(block
(set total (add total i))
(set i (add i (i64 1)))))
total))))))

main.axs:

(module
(import math "math.axs")
(fn main () i32
(block
(call print_i64 (call math.square (i64 7))) ;; prints 49
(call print_i64 (call math.sum_to (i64 100))) ;; prints 5050
(i32 0))))

Compile and run:

Terminal window
./build/axonc main.axs -o program
./program

The compiler resolves "math.axs" relative to main.axs, loads it, and links everything together.


;; The simplest AXON program.
;; Prints "Hello, World!" and exits with code 0.
(module
(fn main () i32
(block
(call print_str (str "Hello, World!"))
(i32 0))))
;; Demonstrates typed literals and arithmetic operators.
(module
(fn main () i32
(block
;; Addition: 3 + 7 = 10
(call print_i64 (add (i64 3) (i64 7)))
;; Multiplication: 6 * 9 = 54
(call print_i64 (mul (i64 6) (i64 9)))
;; Nested: (2 + 3) * (4 + 5) = 45
(call print_i64 (mul (add (i64 2) (i64 3))
(add (i64 4) (i64 5))))
(i32 0))))
;; A function that computes the square of a number.
(module
(fn square ((x i64)) i64
(mul x x))
(fn main () i32
(block
(call print_i64 (call square (i64 7))) ;; prints 49
(call print_i64 (call square (i64 12))) ;; prints 144
(i32 0))))
;; Classic recursive fibonacci.
;; fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2)
(module
(fn fib ((n i64)) i64
(if (lt n (i64 2))
n
(add (call fib (sub n (i64 1)))
(call fib (sub n (i64 2))))))
(fn main () i32
(block
(call print_i64 (call fib (i64 10))) ;; prints 55
(call print_i64 (call fib (i64 20))) ;; prints 6765
(call print_i64 (call fib (i64 35))) ;; prints 9227465
(i32 0))))
;; Recursive factorial.
;; fact(0) = 1, fact(n) = n * fact(n-1)
(module
(fn factorial ((n i64)) i64
(if (le n (i64 1))
(i64 1)
(mul n (call factorial (sub n (i64 1))))))
(fn main () i32
(block
(call print_i64 (call factorial (i64 5))) ;; prints 120
(call print_i64 (call factorial (i64 10))) ;; prints 3628800
(call print_i64 (call factorial (i64 20))) ;; prints 2432902008176640000
(i32 0))))
;; Demonstrates if expressions producing values.
(module
(fn abs ((x i64)) i64
(if (lt x (i64 0))
(neg x)
x))
(fn max ((a i64) (b i64)) i64
(if (gt a b) a b))
(fn sign ((x i64)) i64
(if (lt x (i64 0))
(i64 -1)
(if (gt x (i64 0))
(i64 1)
(i64 0))))
(fn main () i32
(block
(call print_i64 (call abs (i64 -42))) ;; prints 42
(call print_i64 (call abs (i64 17))) ;; prints 17
(call print_i64 (call max (i64 10) (i64 20))) ;; prints 20
(call print_i64 (call sign (i64 -5))) ;; prints -1
(call print_i64 (call sign (i64 0))) ;; prints 0
(call print_i64 (call sign (i64 99))) ;; prints 1
(i32 0))))
;; Iterative sum using while loop and mutable variables.
;; sum(n) = 1 + 2 + ... + n = n*(n+1)/2
(module
(fn sum_to ((n i64)) i64
(let (total (i64 0))
(let (i (i64 1))
(block
(while (le i n)
(block
(set total (add total i))
(set i (add i (i64 1)))))
total))))
(fn main () i32
(block
(call print_i64 (call sum_to (i64 10))) ;; prints 55
(call print_i64 (call sum_to (i64 100))) ;; prints 5050
(call print_i64 (call sum_to (i64 1000))) ;; prints 500500
(i32 0))))

Example 8: Let Bindings and Nested Expressions

Section titled “Example 8: Let Bindings and Nested Expressions”
;; Demonstrates variable binding and complex nesting.
;; Computes the distance formula: sqrt(dx*dx + dy*dy)
;; (Approximated as dx*dx + dy*dy since we don't have sqrt yet)
(module
(fn dist_squared ((x1 i64) (y1 i64) (x2 i64) (y2 i64)) i64
(let (dx (sub x2 x1))
(let (dy (sub y2 y1))
(add (mul dx dx) (mul dy dy)))))
(fn main () i32
(block
;; Distance² from (0,0) to (3,4) = 9 + 16 = 25
(call print_i64 (call dist_squared (i64 0) (i64 0) (i64 3) (i64 4)))
;; Distance² from (1,2) to (4,6) = 9 + 16 = 25
(call print_i64 (call dist_squared (i64 1) (i64 2) (i64 4) (i64 6)))
(i32 0))))
;; Demonstrates explicit type conversions.
(module
(fn main () i32
(block
;; Integer widening: i32 → i64
(let (small (i32 42))
(let (big (cast i64 small))
(call print_i64 big))) ;; prints 42
;; Integer to float
(let (n (i64 100))
(let (f (cast f64 n))
(call print_f64 f))) ;; prints 100.0
;; Float to integer (truncates)
(let (pi (f64 3.14159))
(let (truncated (cast i64 pi))
(call print_i64 truncated))) ;; prints 3
(i32 0))))
;; Demonstrates importing and calling C standard library functions.
(module
;; Import C's puts function
(extern puts ((s (ptr i8))) i32)
;; Import C's exit function
(extern exit ((code i32)) void)
(fn main () i32
(block
;; Call puts directly
(call puts (str "Direct call to C's puts!"))
;; Conditional exit
(let (should_exit (bool false))
(if (eq should_exit (bool true))
(call exit (i32 1))))
(call puts (str "Program completed normally."))
(i32 0))))

Example 11: Multiple Functions — FizzBuzz

Section titled “Example 11: Multiple Functions — FizzBuzz”
;; FizzBuzz: print Fizz, Buzz, FizzBuzz, or the number for 1..N.
;; Since AXON doesn't have string interpolation, we print indicators.
(module
(fn fizzbuzz ((n i64)) void
(let (i (i64 1))
(while (le i n)
(block
(if (eq (mod i (i64 15)) (i64 0))
(call print_str (str "FizzBuzz"))
(if (eq (mod i (i64 3)) (i64 0))
(call print_str (str "Fizz"))
(if (eq (mod i (i64 5)) (i64 0))
(call print_str (str "Buzz"))
(call print_i64 i))))
(set i (add i (i64 1)))))))
(fn main () i32
(block
(call fizzbuzz (i64 30))
(i32 0))))

Example 12: Iterative Fibonacci (Efficient)

Section titled “Example 12: Iterative Fibonacci (Efficient)”
;; Iterative fibonacci using a while loop — O(n) instead of O(2^n).
(module
(fn fib_iter ((n i64)) i64
(if (lt n (i64 2))
n
(let (a (i64 0))
(let (b (i64 1))
(let (i (i64 2))
(block
(while (le i n)
(let (tmp (add a b))
(block
(set a b)
(set b tmp)
(set i (add i (i64 1))))))
b))))))
(fn main () i32
(block
(call print_i64 (call fib_iter (i64 10))) ;; prints 55
(call print_i64 (call fib_iter (i64 50))) ;; prints 12586269025
(i32 0))))

Example 13: Struct Literals and Field Access

Section titled “Example 13: Struct Literals and Field Access”
;; Demonstrates struct definition, construction, and field access.
(module
(struct Point ((x i64) (y i64)))
(fn manhattan_distance ((p Point)) i64
(add (fld p x) (fld p y)))
(fn main () i32
(block
(let (p (new Point (x (i64 3)) (y (i64 4))))
(call print_i64 (call manhattan_distance p))) ;; prints 7
(i32 0))))
;; Demonstrates array creation and element access.
(module
(fn sum_array () i64
(let (nums (arr.new i64 (i64 10) (i64 20) (i64 30)))
(add (add (idx nums (i64 0)) (idx nums (i64 1)))
(idx nums (i64 2)))))
(fn main () i32
(block
(call print_i64 (call sum_array)) ;; prints 60
(i32 0))))
;; Demonstrates constant declarations.
(module
(const MAX_VAL i64 (i64 100))
(const SCALE i64 (i64 10))
(fn clamp_and_scale ((x i64)) i64
(if (gt x MAX_VAL)
(mul MAX_VAL SCALE)
(mul x SCALE)))
(fn main () i32
(block
(call print_i64 (call clamp_and_scale (i64 50))) ;; prints 500
(call print_i64 (call clamp_and_scale (i64 200))) ;; prints 1000
(i32 0))))
;; Demonstrates addr (address-of) and deref (dereference).
(module
(fn swap_via_ptr ((a (ptr i64)) (b (ptr i64))) void
(let (tmp (deref a))
(block
(set (deref a) (deref b))
(set (deref b) tmp))))
(fn main () i32
(block
(let (x (i64 10))
(let (y (i64 20))
(block
(call print_i64 x) ;; prints 10
(call print_i64 y) ;; prints 20
(call print_i64 (deref (addr x))) ;; prints 10 (addr then deref)
)))
(i32 0))))

(fn sum_to ((n i64)) i64
(let (total (i64 0))
(let (i (i64 1))
(block
(while (le i n)
(block
(set total (add total i))
(set i (add i (i64 1)))))
total))))

Pattern: Initialize accumulator → loop with mutation → return accumulator.

(fn max ((a i64) (b i64)) i64
(if (gt a b) a b))
(fn min ((a i64) (b i64)) i64
(if (lt a b) a b))

Pattern: Use if as a value-producing expression.

(fn clamp ((x i64) (lo i64) (hi i64)) i64
(if (lt x lo)
lo
(if (gt x hi)
hi
x)))
;; Recursive GCD
(fn gcd ((a i64) (b i64)) i64
(if (eq b (i64 0))
a
(call gcd b (mod a b))))
;; Iterative GCD
(fn gcd_iter ((a i64) (b i64)) i64
(let (x a)
(let (y b)
(block
(while (ne y (i64 0))
(let (tmp (mod x y))
(block
(set x y)
(set y tmp))))
x))))
(fn power ((base i64) (exp i64)) i64
(let (result (i64 1))
(let (b base)
(let (e exp)
(block
(while (gt e (i64 0))
(block
(if (eq (mod e (i64 2)) (i64 1))
(set result (mul result b)))
(set b (mul b b))
(set e (div e (i64 2)))))
result)))))
;; Binary search: returns 1 if target found in sorted array-like sequence,
;; 0 otherwise. This demonstrates the pattern using computed values.
(fn binary_search_sum ((target i64) (n i64)) i64
;; Search for target in the sequence 1, 2, 3, ..., n
(let (lo (i64 1))
(let (hi n)
(let (found (i64 0))
(block
(while (le lo hi)
(let (mid (div (add lo hi) (i64 2)))
(if (eq mid target)
(block
(set found (i64 1))
(set lo (add hi (i64 1)))) ;; break: set lo > hi
(if (lt mid target)
(set lo (add mid (i64 1)))
(set hi (sub mid (i64 1)))))))
found)))))
;; Print multiple labeled values
(fn print_labeled ((label (ptr i8)) (value i64)) void
(block
(call print_str label)
(call print_i64 value)))
;; Usage
(fn main () i32
(block
(call print_labeled (str "Result:") (i64 42))
(call print_labeled (str "Count:") (i64 100))
(i32 0)))

Pattern 8: Absolute Value (Generic Integer)

Section titled “Pattern 8: Absolute Value (Generic Integer)”
(fn abs ((x i64)) i64
(if (lt x (i64 0))
(neg x)
x))
(fn is_even ((n i64)) bool
(eq (mod n (i64 2)) (i64 0)))
(fn is_odd ((n i64)) bool
(ne (mod n (i64 2)) (i64 0)))
(fn countdown ((n i64)) void
(let (i n)
(while (gt i (i64 0))
(block
(call print_i64 i)
(set i (sub i (i64 1)))))))
(fn collatz_steps ((n i64)) i64
(let (x n)
(let (steps (i64 0))
(block
(while (ne x (i64 1))
(block
(if (eq (mod x (i64 2)) (i64 0))
(set x (div x (i64 2)))
(set x (add (mul x (i64 3)) (i64 1))))
(set steps (add steps (i64 1)))))
steps))))

Terminal window
# Basic compilation (QBE backend, debug)
./build/axonc program.axs -o program
./program
# LLVM backend (optimized)
./build/axonc program.axs -o program --backend llvm -O2
# Type-check only (no code generation)
./build/axonc program.axs --check
# Emit QBE IL to stdout
./build/axonc program.axs --emit qbe
# Emit LLVM IR to stdout
./build/axonc program.axs --emit llvm
# Dump AST
./build/axonc program.axs --emit ast
# Dump AXON IR
./build/axonc program.axs --emit ir
# Verbose mode (print pipeline stages)
./build/axonc program.axs -o program --verbose
OptionDescription
-o FILEOutput file path
--backend qbe|llvmSelect backend (default: qbe)
-O0, -O1, -O2Optimization level (default: 0)
--checkType-check only, don’t compile
--emit qbe|llvm|ast|irEmit intermediate representation
--target TRIPLESet target triple (default: auto-detect)
--verbosePrint compilation stages
Terminal window
# Build the compiler
make # debug build
make release # optimized build
make clean # remove build artifacts
make test # run test suite
make install # install to /usr/local/bin

The AXON runtime (libaxon_rt.a) is automatically linked. It provides:

  • print_i64(i64) — print integer
  • print_f64(f64) — print float
  • print_str((ptr i8)) — print string
  • print_bool(bool) — print boolean
  • axon_alloc(i64) — allocate zero-initialized heap memory (used by alloc)
  • axon_alloc_n(i64, i64) — allocate zero-initialized array (used by alloc.n)
  • axon_free((ptr i8)) — free heap memory (used by free)

The allocation/free functions are called automatically by the compiler. These functions are always available without extern declarations.


;; WRONG — 42 is interpreted as a variable name, not a number
(add x 42)
;; CORRECT
(add x (i64 42))
;; WRONG — S-expressions require parentheses around every operation
add x y
;; CORRECT
(add x y)

❌ Calling functions without call keyword

Section titled “❌ Calling functions without call keyword”
;; WRONG — looks like a binary operator named "fib"
(fib (i64 35))
;; CORRECT
(call fib (i64 35))
;; WRONG — can't add i32 and i64
(add (i32 1) (i64 2))
;; CORRECT — cast to matching types
(add (cast i64 (i32 1)) (i64 2))

❌ Using if without matching branch types

Section titled “❌ Using if without matching branch types”
;; WRONG — branches have different types (i64 vs i32)
(if (gt x (i64 0))
(i64 1)
(i32 0))
;; CORRECT — both branches must have the same type
(if (gt x (i64 0))
(i64 1)
(i64 0))
;; WRONG — no module wrapper
(fn main () i32
(i32 0))
;; CORRECT — everything must be inside (module ...)
(module
(fn main () i32
(i32 0)))
;; WRONG — no entry point
(module
(fn helper ((x i64)) i64
(mul x x)))
;; CORRECT — must have main returning i32
(module
(fn helper ((x i64)) i64
(mul x x))
(fn main () i32
(block
(call print_i64 (call helper (i64 5)))
(i32 0))))
;; WRONG — integer is not bool
(if (i64 1)
(call print_str (str "yes")))
;; CORRECT — use a comparison to produce bool
(if (ne x (i64 0))
(call print_str (str "yes")))
;; WRONG — bool needs the bool type prefix
(if true
(i64 1)
(i64 0))
;; CORRECT
(if (bool true)
(i64 1)
(i64 0))

Wait — actually bare true and false are parsed as the tokens TOK_TRUE/TOK_FALSE and can be used directly as boolean expressions in some positions. However, the safest and most explicit approach is:

;; Always safest to use comparison expressions for conditions
(if (eq x (i64 0))
...)
;; WRONG — x is not defined
(set x (i64 42))
;; CORRECT — introduce with let first
(let (x (i64 0))
(block
(set x (i64 42))
x))
;; WRONG — missing closing paren
(fn main () i32
(add (i64 1) (i64 2))
;; CORRECT — every ( has a matching )
(fn main () i32
(add (i64 1) (i64 2)))

❌ Calling a private function from another module

Section titled “❌ Calling a private function from another module”
;; WRONG — internal_helper is not pub in math.axs
(import math "math.axs")
(call math.internal_helper (i64 5))
;; CORRECT — only call pub functions
(call math.square (i64 5))
;; WRONG — function is private, other modules can't see it
(module
(fn my_api ((x i64)) i64
(mul x x)))
;; CORRECT — wrap in pub
(module
(pub (fn my_api ((x i64)) i64
(mul x x))))
;; WRONG — math is not imported
(call math.sqrt (f64 2.0))
;; CORRECT — import first
(import math "math.axs")
(call math.sqrt (f64 2.0))

❌ Deferring inside a loop without understanding accumulation

Section titled “❌ Deferring inside a loop without understanding accumulation”
;; WRONG — defers accumulate every iteration, all run at function exit
;; This registers N defers, not one per iteration.
(fn process_all ((n i64)) void
(let (i (i64 0))
(while (lt i n)
(block
(let (h (call acquire_resource i))
(defer (call release_resource h))) ;; N defers pile up!
(set i (add i (i64 1)))))))
;; CORRECT — clean up explicitly in each iteration
(fn process_all ((n i64)) void
(let (i (i64 0))
(while (lt i n)
(block
(let (h (call acquire_resource i))
(block
(call do_work h)
(call release_resource h))) ;; explicit cleanup per iteration
(set i (add i (i64 1)))))))
;; ALSO CORRECT — extract loop body to a helper function
(fn process_one ((i i64)) void
(let (h (call acquire_resource i))
(block
(defer (call release_resource h)) ;; defer is per-function, so OK
(call do_work h))))
(fn process_all ((n i64)) void
(let (i (i64 0))
(while (lt i n)
(block
(call process_one i)
(set i (add i (i64 1)))))))

13. Performance Tips — Generating Fast Code

Section titled “13. Performance Tips — Generating Fast Code”

Integer arithmetic maps to single CPU instructions. Float operations may be slower depending on the pipeline:

;; FASTER — integer multiply
(mul (i64 100) (i64 3))
;; SLOWER — float multiply (may involve FPU pipeline stalls)
(mul (f64 100.0) (f64 3.0))

12.2 Avoid Deep Recursion — Use Iterative Loops

Section titled “12.2 Avoid Deep Recursion — Use Iterative Loops”

Recursive functions create stack frames. Iterative versions are faster for large inputs:

;; SLOW — O(2^n) recursive Fibonacci, deep stack
(fn fib_slow ((n i64)) i64
(if (lt n (i64 2)) n
(add (call fib_slow (sub n (i64 1)))
(call fib_slow (sub n (i64 2))))))
;; FAST — O(n) iterative Fibonacci, constant stack
(fn fib_fast ((n i64)) i64
(if (lt n (i64 2)) n
(let (a (i64 0))
(let (b (i64 1))
(let (i (i64 2))
(block
(while (le i n)
(let (tmp (add a b))
(block (set a b) (set b tmp) (set i (add i (i64 1))))))
b))))))

12.3 Use Bitwise Operations for Power-of-Two Math

Section titled “12.3 Use Bitwise Operations for Power-of-Two Math”

Note: With -O1, the compiler automatically converts mul x, 2^n to shl x, n. Manual bitwise tricks are still useful for division and modulo, which aren’t auto-optimized yet.

;; SLOW — division
(div x (i64 8))
;; FAST — right shift (equivalent for positive integers)
(shr x (i64 3))
;; SLOW — modulo
(mod x (i64 16))
;; FAST — bitwise AND (equivalent for power-of-two modulus)
(band x (i64 15))
;; SLOW — multiplication
(mul x (i64 4))
;; FAST — left shift
(shl x (i64 2))

Each let creates a scope. Flat blocks are more register-friendly:

;; LESS EFFICIENT — deep nesting
(let (a (call compute_a))
(let (b (call compute_b))
(let (c (call compute_c))
(add (add a b) c))))
;; MORE EFFICIENT — direct expression composition
(add (add (call compute_a) (call compute_b)) (call compute_c))

The AXON compiler’s -O1 pass applies several optimization techniques:

Terminal window
# Without optimization:
./build/axonc program.axs --emit-qbe -O0
# With optimization:
./build/axonc program.axs --emit-qbe -O1

Constant Folding — evaluates compile-time constant expressions:

;; (add (i64 3) (i64 7)) → (i64 10) at compile time
;; (not true) → false
;; (neg (i64 5)) → (i64 -5)
;; (fadd (f64 1.5) (f64 2.5)) → (f64 4.0)
;; (flt (f64 1.0) (f64 2.0)) → true

Strength Reduction — replaces expensive operations with cheaper ones:

;; (mul x (i64 8)) → (shl x (i64 3)) — shift instead of multiply
;; (mul (i64 16) x) → (shl x (i64 4)) — commutative swap + shift
;; (mul x (i64 0)) → (i64 0) — zero annihilator

Dead Code Elimination — removes unreachable and unused code:

;; Constant-condition branches are folded to direct jumps:
;; (if true a b) → a (else branch is dead, removed)
;; Unused intermediate values are eliminated after folding.

14. Memory Layout — How Types Map to Memory

Section titled “14. Memory Layout — How Types Map to Memory”
TypeSize (bytes)AlignmentQBE TypeLLVM Type
bool11w (word, zero-extended)i1
i8 / u811w (word, sign/zero-ext)i8
i16 / u1622w (word, sign/zero-ext)i16
i32 / u3244w (word)i32
i64 / u6488l (long)i64
f3244s (single)float
f6488d (double)double
(ptr T)88l (long)ptr
void00void

AXON follows the platform’s C calling convention:

  • x86-64 System V ABI (Linux): First 6 integer args in %rdi, %rsi, %rdx, %rcx, %r8, %r9. First 8 float args in %xmm0%xmm7.
  • ARM64 ABI (macOS): First 8 integer args in x0x7. First 8 float args in d0d7.
  • Return values: Integer/pointer in rax/x0. Float in xmm0/d0.
┌──────────────────┐ ← Stack grows down
│ Return address │
├──────────────────┤
│ Saved registers │
├──────────────────┤
│ Local variables │ ← let bindings live here
│ (most recent at │
│ lowest address) │
├──────────────────┤
│ Arguments for │ ← when calling other functions
│ outgoing calls │
└──────────────────┘

15. QBE IL Patterns — What AXON Generates

Section titled “15. QBE IL Patterns — What AXON Generates”

Understanding the generated QBE IL helps debug compilation issues.

AXON source:

(fn add_one ((x i64)) i64
(add x (i64 1)))

Generated QBE IL:

function l $add_one(l %x) {
@start
%0 =l add %x, 1
ret %0
}

AXON source:

(fn max ((a i64) (b i64)) i64
(if (gt a b) a b))

Generated QBE IL:

function l $max(l %a, l %b) {
@start
%0 =w csgtl %a, %b
jnz %0, @then_0, @else_0
@then_0
%1 =l copy %a
jmp @merge_0
@else_0
%2 =l copy %b
jmp @merge_0
@merge_0
%3 =l phi @then_0 %1, @else_0 %2
ret %3
}

AXON source:

(let (i (i64 0))
(while (lt i (i64 10))
(set i (add i (i64 1)))))

Generated QBE IL:

%i =l copy 0
@while_cond_0
%0 =w csltl %i, 10
jnz %0, @while_body_0, @while_end_0
@while_body_0
%1 =l add %i, 1
storel %1, %i_addr
jmp @while_cond_0
@while_end_0
SuffixMeaningCorresponds to
wWord (32-bit)i32, u32, bool
lLong (64-bit)i64, u64, pointers
sSingle (32-bit float)f32
dDouble (64-bit float)f64
QBE InstructionMeaning
ceqw / ceqlCompare equal (word/long)
cnew / cnelCompare not equal
csltw / csltlCompare signed less than
cslew / cslelCompare signed less or equal
csgtw / csgtlCompare signed greater than
csgew / csgelCompare signed greater or equal
cultw / cultlCompare unsigned less than
culew / culelCompare unsigned less or equal

Source (.axs) → Lexer → Parser → AST → Type Checker → IR → Backend → Output
↓ ↓ ↓
--emit-ast --emit-ir --emit-qbe/llvm

Each --emit-* flag lets you inspect the output at that stage.

Terminal window
# Type-check only — fastest way to find type errors
./build/axonc program.axs --check

Common type error messages:

  • "type mismatch" — binary operands have different types
  • "expected bool" — if/while condition is not boolean
  • "wrong number of arguments" — function call has wrong arg count
  • "undefined variable" — referencing a variable not in scope
  • "undefined function" — calling a function that doesn’t exist
Terminal window
# Inspect the AXON IR (architecture-independent)
./build/axonc program.axs --emit-ir
# Inspect QBE IL (before assembling)
./build/axonc program.axs --emit-qbe -o output.ssa
# Inspect LLVM IR
./build/axonc program.axs --emit-llvm -o output.ll
# Inspect the AST (parsed structure)
./build/axonc program.axs --emit-ast
Terminal window
# Print each compilation stage and timing
./build/axonc program.axs -o program --verbose
  1. Type error? → Run --check first
  2. Wrong output? → Compare --emit-ir against expected logic
  3. Crash at runtime? → Check --emit-qbe for malformed IL
  4. Build fails? → Check --emit-llvm for LLVM IR validity
  5. Performance issue? → Compare -O0 vs -O1 --emit-qbe output
  6. Post-mortem crash from production/CI? → Use tools/addr2line.py to resolve crash addresses to source locations (requires debug-enabled binary). See addr2line-tool.md for details.

(module
;; extern declarations (optional)
(extern puts ((s (ptr i8))) i32)
;; helper functions
(fn helper ((param type)) return_type
body)
;; entry point (required)
(fn main () i32
(block
;; your code here
(i32 0)))) ;; exit code
TypeSyntax
Integer(i64 42), (i32 -7), (u8 255)
Float(f64 3.14), (f32 1.0)
Bool(bool true), (bool false)
String(str "hello")
CategoryOperators
Arithmeticadd, sub, mul, div, mod
Comparisoneq, ne, lt, le, gt, ge
Bitwiseband, bor, bxor, shl, shr
Logicalland, lor, not
Unaryneg, not, bnot
ConstructSyntax
If-else(if cond then else)
If (void)(if cond then)
While(while cond body)
Block(block e1 e2 ... en)
Match(match expr (pattern body) ...)
Return(ret expr) or (ret)
Defer(defer expr) — runs at function exit, LIFO order
ConstructSyntax
Match on enum(match color ((Color Red) body1) ((Color Green) body2) ...)
Match on integer(match x ((i32 0) body1) ((i32 1) body2) (_ default_body))
Match on bool(match flag (true body1) (false body2))
Wildcard arm(_ body) — matches anything
Match as expr(let (r (match x ...)) ...)

Key rules:

  • All arm bodies must have the same type.
  • Patterns must match scrutinee type.
  • Enum match requires all variants covered (or _ wildcard).
  • Bool match requires both true and false covered (or _).
  • _ is optional for integer types.
  • Match is an expression — its value is the matched arm’s body.
ConstructSyntax
Introduce(let (name init) body)
With type(let (name type init) body)
Mutate(set name value)
ConstructSyntax
Define(fn name ((p1 t1) ...) ret_type body)
Public fn(pub (fn name ((p1 t1) ...) ret_type body))
Call(call name arg1 arg2 ...)
Extern(extern name ((p1 t1) ...) ret_type)
Constant(const name type value)
ConstructSyntax
Namespaced import(import alias "path.axs")
Selective import(use "path.axs" (fn1 fn2 ...))
Qualified call(call alias.fn_name args...)
Make fn public(pub (fn ...))
CategoryExamples
Signed inti8, i16, i32, i64
Unsigned intu8, u16, u32, u64
Floatf32, f64
Othervoid, bool
Pointer(ptr i8), (ptr (ptr i64))
Array(arr i32 10)
Slice(slice u8)
Function Pointer(fnptr (i64) i64), (fnptr (i64 i64) bool)
ConstructSyntax
Struct decl(struct Name ((f1 t1) ...))
Struct literal(new Name (f1 v1) (f2 v2) ...)
Field access(fld expr field_name)
Array literal(arr.new T v1 v2 ...)
Array index(idx expr index_expr)
Address-of(addr var_name)
Dereference(deref ptr_expr)
ConstructSyntax
Single alloc(alloc T)(ptr T) (zero-initialized)
Array alloc(alloc.n T count_expr)(ptr T) (zero-initialized)
Free memory(free ptr_expr)void

Key rules:

  • Memory is zero-initialized (uses calloc).
  • OOM causes a runtime panic.
  • You must free what you allocate (no GC).
  • Double-free and use-after-free are UB.
ConstructSyntax
Type annotation(fnptr (i64 i64) i64)
Variable binding(let (f (fnptr (i64) i64) my_fn) ...)
Indirect call(call f (i64 42))
Higher-order fn(fn apply ((f (fnptr (i64) i64)) (x i64)) i64 (call f x))
Pass fn as arg(call apply double (i64 21))

Key rules:

  • A bare function name in value position is auto-coerced to (fnptr ...).
  • Indirect calls use the same (call ...) syntax as direct calls — the type checker determines which.
  • Function pointer types are structurally compared (same params + return = same type).
ConstructSyntax
Simple lambda(fn ((x i64)) i64 (mul x x))
Multi-param(fn ((a i64) (b i64)) i64 (add a b))
No params(fn () i64 (i64 42))
Bind to variable(let (sq (fn ((x i64)) i64 (mul x x))) (call sq (i64 7)))
Pass to HOF(call apply (fn ((x i64)) i64 (mul x (i64 2))) (i64 5))

Key rules:

  • Lambda syntax is (fn ((params...)) ret_type body) — same as fn but without a name.
  • Evaluates to a (fnptr ...) value with matching parameter and return types.
  • Non-capturing — lambdas cannot reference variables from the enclosing scope.
  • The body type must match the declared return type.
  • Can be used anywhere a function pointer is expected: let bindings, function arguments, data structures.
ConstructSyntax
Test declaration(test "name" body)
Test with block(test "name" (block expr1 expr2 ...))
Assert (boolean)(assert cond) or (assert cond "msg")
Assert equal(assert_eq left right) or (assert_eq left right "msg")
Assert not equal(assert_ne left right) or (assert_ne left right "msg")
Run testsaxonc test file.axs

Key rules:

  • (test ...) is a top-level declaration (same level as fn, struct, etc.).
  • Test files have NO main function — the compiler generates a synthetic main when run with axonc test.
  • Each test body is a single expression. Use (block ...) for multiple expressions.
  • assert_eq / assert_ne support i32, i64, u64, and bool types. Both sides must have the same type.
  • On failure, assert_eq shows both values: "assert_eq failed: left=10, right=20".
  • Tests are isolated: a failing test does NOT abort remaining tests. The runner prints a pass/fail summary.
  • Exit code: 0 if all tests pass, 1 if any fail.

Example test file:

(module
(test "arithmetic"
(block
(assert (eq (add (i32 2) (i32 2)) (i32 4)))
(assert_eq (add (i64 10) (i64 20)) (i64 30))))
(test "comparisons"
(assert_ne (i64 42) (i64 99)))
)
ConstructSyntax
Enum declaration(enum Color i32 ((Red 0) (Green 1) (Blue 2)))
Enum literal(Color Red), (Color Blue)
Equality compare(eq c (Color Red)), (ne c (Color Green))
Cast to integer(cast i64 (Color Green))1
Cast from integer(cast Color (i32 2))
As function param(fn f ((c Color)) i64 (cast i64 c))
In let binding(let (c Color (Color Red)) ...)

Key rules:

  • Enum types use nominal typing — different enum names are different types even with same structure.
  • Only eq and ne are allowed on enum values. No arithmetic, no ordered comparison.
  • Cast to/from backing integer type for interop with integer operations.
  • Enum values are stored as their backing integer type (same size and alignment).

Axon is being designed to support fully compiled, cross-platform GUI applications. This capability is planned for milestones M9-M12.

  • Platform Layer (libaxon_pal): C library for OS windowing, events, input
  • Rendering Engine (libaxon_render): Software 2D rasterizer + GPU backends
  • UI Framework (axon.ui): Immediate-mode widget API with flexbox layout
  • Widget Library: 17+ standard widgets, theming, app bundling
PlatformBackendAPI
macOSAppKit via ObjC runtimeobjc_msgSend
LinuxX11 (Xlib) / WaylandXCreateWindow
WindowsWin32CreateWindowEx

GUI support requires language features from M2-M5:

  • Function pointers (M2) — for event callbacks
  • Enums (M2) — for event types
  • Heap allocation (M5) — for widget trees
  • String type (M5) — for text handling
  • Module system (M4) — for axon.ui imports

See docs/gui-architecture.md for the complete architecture plan.