Language Spec
AXON Language Specification
Section titled “AXON Language Specification”Version 0.3.0 — Phase 2 Progress
Table of Contents
Section titled “Table of Contents”- Overview & Design Philosophy
- Source Formats
- Lexical Grammar
- Syntax Grammar
- Type System
- Expressions
- Declarations
- Module System
- Operator Reference
- Built-in Functions
- Memory Model
- Error Handling
- Concurrency Model
1. Overview & Design Philosophy
Section titled “1. Overview & Design Philosophy”AXON (AI-Native eXecution Optimized Notation) is a programming language designed for AI models to generate high-performance native applications. It is explicitly not designed for human readability or ergonomics.
Design Principles
Section titled “Design Principles”-
AI-Native — Syntax and semantics are optimized for machine generation. The grammar is regular and context-free; every construct is fully parenthesized; there is no syntactic sugar that introduces ambiguity.
-
Performance-First — The language compiles to native code via QBE or LLVM. There is no garbage collector, no runtime reflection, no dynamic dispatch. All memory management is explicit (arena-based in Phase 1, linear types in Phase 2).
-
Fully Explicit — Every expression has a known type. Every literal carries a type annotation. There are no implicit conversions, no type inference beyond what is structurally determined, and no overloading.
-
Minimal — The language has a small number of orthogonal constructs. There is one way to define functions, one way to bind variables, one way to create loops. This minimizes the state space an AI model must navigate.
-
Deterministic Compilation — The same source always produces the same IR. There are no order-dependent name resolution rules, no macro expansion phases, and no preprocessor.
Non-Goals
Section titled “Non-Goals”- Human readability
- Rich syntax sugar (operator overloading, method chaining, pattern matching in Phase 1)
- Dynamic features (reflection, eval, dynamic types)
- Garbage collection
2. Source Formats
Section titled “2. Source Formats”2.1 S-Expression Text Format (.axs) — Phase 1
Section titled “2.1 S-Expression Text Format (.axs) — Phase 1”The current source format is S-expressions: fully parenthesized prefix notation. This format is:
- Unambiguous — no operator precedence, no dangling else, no semicolon insertion
- Easy to parse — a recursive descent parser handles the entire language
- Easy to generate — an AI model needs only emit balanced parentheses and known keywords
Files use the .axs extension. The encoding is UTF-8.
(module (fn add_one ((x i64)) i64 (add x (i64 1))))2.2 Binary AST Format (.axb) — Phase 2
Section titled “2.2 Binary AST Format (.axb) — Phase 2”Phase 2 introduces a compact binary encoding of the AST. The AI model emits a byte stream of opcodes, type table indices, and De Bruijn variable references. This eliminates lexing and parsing entirely — the compiler loads the binary directly into its internal representation.
See binary-format.md for the complete specification.
2.3 Comments
Section titled “2.3 Comments”In .axs files, line comments begin with ;; and extend to the end of the line:
;; This is a comment(fn main () i32 ;; inline comment (i32 0))There are no block comments.
3. Lexical Grammar
Section titled “3. Lexical Grammar”The lexer produces a flat stream of tokens from .axs source text.
3.1 Token Types
Section titled “3.1 Token Types”| Token | Pattern | Examples |
|---|---|---|
LPAREN | ( | ( |
RPAREN | ) | ) |
IDENT | [a-zA-Z_][a-zA-Z0-9_]* | fn, add, my_var, i64 |
INT | [+-]?[0-9]+ or 0x[0-9a-fA-F]+ or 0b[01]+ | 42, -7, 0xFF, 0b1010 |
FLOAT | [0-9]+\.[0-9]*([eE][+-]?[0-9]+)? | 3.14, 1e-5, 2.0 |
STRING | "[^"]*" | "hello", "Hello, World!" |
DOT_DOT_DOT | ... | ... |
TRUE | true | true |
FALSE | false | false |
EOF | end of input | — |
3.2 Whitespace
Section titled “3.2 Whitespace”Whitespace characters (space, tab, newline, carriage return) separate tokens but are otherwise ignored. Any amount of whitespace is permitted between tokens.
3.3 Identifiers
Section titled “3.3 Identifiers”Identifiers start with a letter or underscore, followed by zero or more letters, digits, or underscores. Identifiers are case-sensitive.
Keywords (like fn, module, if, while, let, set, block, ret, cast, call, extern, struct, defer) are parsed contextually — they are valid identifiers that acquire special meaning only in specific syntactic positions.
3.4 Integer Literals
Section titled “3.4 Integer Literals”Integer literals may be decimal, hexadecimal (prefix 0x), or binary (prefix 0b). Negative integers are expressed with a leading - sign. All integer literals must appear inside a typed expression: (i64 42), (i32 -7), (u8 255).
3.5 Float Literals
Section titled “3.5 Float Literals”Float literals contain a decimal point and/or an exponent. They must appear inside a typed expression: (f64 3.14), (f32 1e-5).
3.6 String Literals
Section titled “3.6 String Literals”String literals are enclosed in double quotes. The following escape sequences are recognized:
| Escape | Meaning |
|---|---|
\\ | Backslash |
\" | Double quote |
\n | Newline (LF) |
\r | Carriage return |
\t | Tab |
\0 | Null byte |
3.7 Boolean Literals
Section titled “3.7 Boolean Literals”The tokens true and false are boolean literals. They must appear inside a typed expression: (bool true), (bool false).
4. Syntax Grammar
Section titled “4. Syntax Grammar”The following grammar is specified in EBNF. Terminals are in 'quotes' or UPPERCASE. Nonterminals are in lowercase_with_underscores.
4.1 Module
Section titled “4.1 Module”module ::= '(' 'module' decl* ')'A module is the top-level compilation unit. It contains zero or more declarations.
4.2 Declarations
Section titled “4.2 Declarations”decl ::= fn_decl | extern_decl | struct_decl | const_decl | import_decl | use_decl
fn_decl ::= '(' 'fn' IDENT '(' param* ')' type expr ')'
param ::= '(' IDENT type ')'
extern_decl::= '(' 'extern' IDENT '(' param_or_type* variadic? ')' type ')'
param_or_type ::= '(' IDENT type ')' | type
variadic ::= '...'
struct_decl::= '(' 'struct' IDENT '(' field* ')' ')'
enum_decl ::= '(' 'enum' IDENT type '(' variant* ')' ')'
variant ::= '(' IDENT INT ')'
field ::= '(' IDENT type ')'
const_decl ::= '(' 'const' IDENT type expr ')'
import_decl ::= '(' 'import' IDENT STRING ')'
use_decl ::= '(' 'use' STRING '(' IDENT* ')' ')'4.3 Types
Section titled “4.3 Types”type ::= 'void' | 'bool' | 'i8' | 'i16' | 'i32' | 'i64' | 'u8' | 'u16' | 'u32' | 'u64' | 'f32' | 'f64' | '(' 'ptr' type ')' | '(' 'arr' type INT ')' | '(' 'slice' type ')' | IDENT (* struct or enum type name *)4.4 Expressions
Section titled “4.4 Expressions”expr ::= int_lit | float_lit | bool_lit | str_lit | var_ref | binop | unop | call | if_expr | while_expr | let_expr | set_expr | block | ret | defer_stmt | cast | index | field_access | deref | addr_of | fn_ref | lambda | array_lit | struct_lit
lambda ::= '(' 'fn' '(' param* ')' type expr ')'
int_lit ::= '(' type INT ')'
float_lit ::= '(' type FLOAT ')'
bool_lit ::= '(' 'bool' BOOL ')'
str_lit ::= '(' 'str' STRING ')'
var_ref ::= IDENT
binop ::= '(' BINOP expr expr ')'
unop ::= '(' UNOP expr ')'
call ::= '(' 'call' IDENT expr* ')'
match_expr ::= '(' 'match' expr match_arm+ ')'
match_arm ::= '(' pattern expr ')'
pattern ::= '_' | expr
if_expr ::= '(' 'if' expr expr expr? ')'
while_expr ::= '(' 'while' expr expr ')'
let_expr ::= '(' 'let' '(' IDENT type? expr ')' expr ')'
set_expr ::= '(' 'set' IDENT expr ')'
block ::= '(' 'block' expr+ ')'
ret ::= '(' 'ret' expr? ')'
defer_stmt ::= '(' 'defer' expr ')'
cast ::= '(' 'cast' type expr ')'
index ::= '(' 'idx' expr expr ')'
field_access ::= '(' 'fld' expr IDENT ')'
deref ::= '(' 'deref' expr ')'
addr_of ::= '(' 'addr' IDENT ')'
array_lit ::= '(' 'arr.new' type expr* ')'
struct_lit ::= '(' 'new' IDENT field_init* ')'
field_init ::= '(' IDENT expr ')'4.5 Operators
Section titled “4.5 Operators”BINOP ::= 'add' | 'sub' | 'mul' | 'div' | 'mod' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'band' | 'bor' | 'bxor' | 'shl' | 'shr' | 'land' | 'lor'
UNOP ::= 'neg' | 'not' | 'bnot'
BOOL ::= 'true' | 'false'5. Type System
Section titled “5. Type System”5.1 Primitive Types
Section titled “5.1 Primitive Types”| Type | Size | Description |
|---|---|---|
void | 0 | No value (used for functions with no return) |
bool | 1 byte | Boolean: true or false |
i8 | 1 byte | Signed 8-bit integer |
i16 | 2 bytes | Signed 16-bit integer |
i32 | 4 bytes | Signed 32-bit integer |
i64 | 8 bytes | Signed 64-bit integer |
u8 | 1 byte | Unsigned 8-bit integer |
u16 | 2 bytes | Unsigned 16-bit integer |
u32 | 4 bytes | Unsigned 32-bit integer |
u64 | 8 bytes | Unsigned 64-bit integer |
f32 | 4 bytes | 32-bit IEEE 754 float |
f64 | 8 bytes | 64-bit IEEE 754 double |
5.2 Compound Types
Section titled “5.2 Compound Types”Pointer — (ptr T)
Section titled “Pointer — (ptr T)”A pointer to a value of type T. Pointer size is platform-dependent (8 bytes on 64-bit systems).
(ptr i64) ;; pointer to i64(ptr (ptr u8)) ;; pointer to pointer to u8Array — (arr T N)
Section titled “Array — (arr T N)”A fixed-size array of N elements of type T. The size N must be a compile-time integer constant.
(arr i32 10) ;; array of 10 i32 values(arr f64 3) ;; array of 3 f64 valuesSlice — (slice T)
Section titled “Slice — (slice T)”A runtime-sized view into a contiguous sequence of T values. Represented as a pointer + length pair.
(slice u8) ;; slice of u8 (byte slice)Struct — user-defined name
Section titled “Struct — user-defined name”A named aggregate type with named fields. Defined via struct declaration, referenced by name.
;; Declaration:(struct Point ((x f64) (y f64)))
;; Usage as type:(fn distance ((p Point)) f64 ...)Function Pointer — (fnptr (P1 P2 ...) R)
Section titled “Function Pointer — (fnptr (P1 P2 ...) R)”A pointer to a function with parameter types P1, P2, ... and return type R. Function pointers are pointer-sized (8 bytes on 64-bit systems).
;; Type annotation:(fnptr (i64) i64) ;; pointer to function taking i64, returning i64(fnptr (i64 i64) bool) ;; pointer to function taking two i64, returning bool(fnptr () void) ;; pointer to function taking nothing, returning void
;; In a let binding — function name becomes a fnptr value:(let (f (fnptr (i64) i64) my_function) (call f (i64 42))) ;; indirect call through function pointer
;; As a function parameter (higher-order functions):(fn apply ((f (fnptr (i64) i64)) (x i64)) i64 (call f x)) ;; indirect call through parameter
;; Passing a function as argument:(call apply double (i64 21)) ;; 'double' is coerced to fnptrRules:
- A function name used outside of call position is automatically coerced to a
(fnptr ...)value. - Indirect calls through a function pointer use the same
(call ...)syntax as direct calls. - Function pointer types are structurally compared — two fnptr types with the same parameter and return types are equal.
- Assigning a function to a fnptr variable checks that the function signature matches the fnptr type exactly.
- Lambda expressions
(fn ((params...)) ret body)evaluate to a(fnptr ...)value. The lambda is lifted to a top-level function at compile time.
Function — internal
Section titled “Function — internal”Function types are created implicitly from function and extern declarations. They are not directly expressible in the source language as type annotations.
Enum — (enum Name BackingType (variants...))
Section titled “Enum — (enum Name BackingType (variants...))”A named type with integer-valued variants. The backing type must be an integer type (e.g., i32, u8). Enum types are nominally typed — two enums with the same structure but different names are different types.
;; Declaration:(enum Color i32 ((Red 0) (Green 1) (Blue 2)))
;; Enum literal (variant constructor):(Color Red) ;; creates a Color value(Color Blue) ;; creates a Color value
;; Use in let bindings:(let (c Color (Color Green)) ...)
;; Use as function parameter:(fn describe_color ((c Color)) i64 (cast i64 c))
;; Equality comparison (eq/ne only):(if (eq c (Color Red)) ...)
;; Cast to/from backing integer type:(cast i64 (Color Green)) ;; → 1Rules:
- Enum types are declared at module level with
(enum Name BackingType (...)). - Each variant has an explicit integer value.
- Enum literals are written as
(EnumName VariantName). - Only
eqandnecomparisons are supported on enum values. - Arithmetic operators are not supported on enums. Cast to integer first.
- Enums can be cast to their backing integer type and vice versa.
- Enum values are stored with the same size and alignment as their backing type.
5.3 Type Rules
Section titled “5.3 Type Rules”-
No implicit conversions. An
i32value cannot be used wherei64is expected. Use(cast i64 expr)explicitly. -
Arithmetic operators require both operands to have the same type, and that type must be numeric (integer or float).
-
Comparison operators (
eq,ne,lt,le,gt,ge) require both operands to have the same type. They producebool. Enum types support onlyeqandne. -
Bitwise operators (
band,bor,bxor,shl,shr) require integer operands of the same type.shlandshrrequire both operands to be the same integer type. -
Logical operators (
not,land,lor) operate onboolvalues. -
ifexpressions: the condition must bebool. If both branches are present, they must have the same type (that becomes the type of theifexpression). If only the then-branch is present, both branches have typevoid. -
letexpressions: the initializer must match the annotated type (if provided). The body expression determines the type of theletexpression. -
callexpressions: argument types must match the function’s parameter types exactly. The call expression has the function’s return type. -
castexpressions: only certain casts are legal:- Integer ↔ Integer (widening, narrowing, sign change)
- Float ↔ Float (
f32↔f64) - Integer → Float, Float → Integer
- Pointer → Pointer (reinterpret)
- Integer ↔ Pointer (platform-dependent)
- Enum ↔ Integer (extract or inject the backing value)
5.4 Linearity Annotations (Phase 2)
Section titled “5.4 Linearity Annotations (Phase 2)”Phase 2 introduces linear types for compile-time memory management:
| Annotation | Meaning |
|---|---|
@once | Value must be used exactly once (linear) |
@maybe | Value may be used zero or one times (affine) |
@many | Value may be used any number of times (unrestricted, default) |
;; Phase 2 syntax (not yet implemented)(fn consume ((@once handle ResourceHandle)) void ...)5.5 Memory Annotations (Phase 2)
Section titled “5.5 Memory Annotations (Phase 2)”| Annotation | Meaning |
|---|---|
@region(name) | Allocate in named region |
@arena | Allocate in arena (bulk free) |
@pool | Allocate in fixed-size pool |
@stack | Allocate on the stack |
5.6 Layout Annotations (Phase 2)
Section titled “5.6 Layout Annotations (Phase 2)”| Annotation | Meaning |
|---|---|
@soa | Struct-of-arrays layout |
@align(N) | Custom alignment |
@hot | Place in hot section (cache-friendly) |
@cold | Place in cold section |
6. Expressions
Section titled “6. Expressions”6.1 Integer Literals
Section titled “6.1 Integer Literals”(i64 42) ;; i64 with value 42(i32 -7) ;; i32 with value -7(u8 255) ;; u8 with value 255(i64 0xFF) ;; i64 with value 255 (hex)(u32 0b11010) ;; u32 with value 26 (binary)The type annotation is mandatory. The literal value must fit in the specified type.
6.2 Float Literals
Section titled “6.2 Float Literals”(f64 3.14) ;; f64(f32 1.0) ;; f32(f64 1e-5) ;; f64 with scientific notation6.3 Boolean Literals
Section titled “6.3 Boolean Literals”(bool true)(bool false)6.4 String Literals
Section titled “6.4 String Literals”(str "Hello, World!")(str "line1\nline2")(str "") ;; empty stringString literals have type (ptr i8) — a pointer to a null-terminated C string.
6.5 Variable References
Section titled “6.5 Variable References”x ;; reference to variable xn ;; reference to variable nmy_counter ;; reference to variable my_counterBare identifiers reference variables in scope. A variable must be defined (via let, function parameter, or loop) before it can be referenced.
6.6 Binary Operations
Section titled “6.6 Binary Operations”(add x y) ;; x + y(sub a b) ;; a - b(mul (i64 3) (i64 7));; 3 * 7(eq x (i64 0)) ;; x == 0(lt n (i64 2)) ;; n < 2(band flags (u32 0xFF)) ;; flags & 0xFF(shl x (i64 3)) ;; x << 3Both operands must have the same type. The result type is the same as the operands, except for comparison operators which produce bool.
6.7 Unary Operations
Section titled “6.7 Unary Operations”(neg x) ;; -x (arithmetic negation)(not cond) ;; !cond (logical NOT, bool → bool)(bnot mask) ;; ~mask (bitwise NOT)6.8 Function Calls
Section titled “6.8 Function Calls”(call fib (i64 35)) ;; call fib with one argument(call add_pair (i64 3) (i64 7)) ;; call add_pair with two arguments(call exit) ;; call exit with no argumentsThe callee must be a function name (not an expression). Arguments are evaluated left-to-right.
6.9 If Expressions
Section titled “6.9 If Expressions”;; With else branch (produces a value)(if (lt n (i64 2)) n (add (call fib (sub n (i64 1))) (call fib (sub n (i64 2)))))
;; Without else branch (void type)(if (gt x (i64 0)) (call print_i64 x))When both branches are present, the if expression produces a value — the type of the expression is the common type of both branches.
6.10 While Loops
Section titled “6.10 While Loops”(while (gt n (i64 0)) (block (call print_i64 n) (set n (sub n (i64 1)))))The condition must be bool. The body is evaluated repeatedly while the condition is true. While loops have type void.
6.11 Let Bindings
Section titled “6.11 Let Bindings”;; With type annotation(let (x i64 (i64 42)) (add x (i64 1)))
;; Without type annotation (inferred from initializer)(let (x (i64 42)) (add x (i64 1)))
;; Nested let bindings(let (a (i64 10)) (let (b (i64 20)) (add a b)))let introduces a new variable binding. The variable is mutable — it can be updated with set. The body expression determines the value and type of the let expression.
6.12 Set (Assignment)
Section titled “6.12 Set (Assignment)”(set x (i64 5)) ;; x = 5(set counter (add counter (i64 1))) ;; counter++The target must be a variable previously introduced by let or a function parameter. The assigned value must have the same type as the variable.
6.13 Block Expressions
Section titled “6.13 Block Expressions”(block (call print_i64 (i64 1)) (call print_i64 (i64 2)) (i64 42))A block evaluates a sequence of expressions in order. The value of the block is the value of the last expression. All preceding expressions must have type void or their values are discarded.
6.14 Match Expressions
Section titled “6.14 Match Expressions”;; Match on enum (exhaustive — all variants covered):(match color ((Color Red) (i64 0)) ((Color Green) (i64 1)) ((Color Blue) (i64 2)))
;; Match on integer (wildcard required):(match x ((i32 0) (i64 100)) ((i32 1) (i64 200)) (_ (i64 999)))
;; Match on bool (exhaustive):(match flag (true (i64 1)) (false (i64 0)))
;; Match with void body (side effects only):(match state ((State Init) (call do_init)) ((State Running) (call do_run)) ((State Done) (call do_done)))match evaluates the scrutinee expression and compares it against each arm’s pattern in order. The body of the first matching arm is evaluated and becomes the value of the match expression.
Rules:
- All arm bodies must have the same type.
- Each pattern must have the same type as the scrutinee.
- Exhaustiveness: For enum types, all variants must be covered (or a
_wildcard present). For bool, bothtrueandfalsemust be covered (or_). For integer types,_is optional but recommended. - The wildcard
_matches anything and must be the last arm. - Match is an expression — it can be used in
letbindings, function arguments, etc.
6.15 Return
Section titled “6.15 Return”(ret (i64 0)) ;; return 0(ret) ;; return voidReturns from the enclosing function. The return value must match the function’s declared return type.
6.15 Cast
Section titled “6.15 Cast”(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)6.16 Index
Section titled “6.16 Index”(idx my_array (i64 3)) ;; my_array[3]The first operand must be an array or pointer type. The second operand must be an integer type. The result type is the element type.
6.17 Field Access
Section titled “6.17 Field Access”(fld my_point x) ;; my_point.xThe first operand must be a struct type. The second operand is a field name.
6.18 Pointer Operations
Section titled “6.18 Pointer Operations”(addr x) ;; &x — take address of variable x(deref ptr) ;; *ptr — dereference pointer6.19 Array Literal
Section titled “6.19 Array Literal”(arr.new i64 (i64 1) (i64 2) (i64 3)) ;; [1, 2, 3]6.20 Struct Literal
Section titled “6.20 Struct Literal”(new Point (x (f64 1.0)) (y (f64 2.0)))6.21 Defer (Scope-Based Cleanup)
Section titled “6.21 Defer (Scope-Based Cleanup)”(defer expr)Registers expr to be executed when the enclosing function exits. Deferred expressions run at every function exit point:
- Explicit return —
(ret ...): the return value is evaluated first, then deferred expressions execute, then control returns to the caller. - Implicit return — falling off the end of the function body: deferred expressions execute after the final expression is evaluated.
- Error propagation —
(try ...): deferred expressions execute before the error is propagated to the caller.
When multiple defer statements are registered in a function, they execute in LIFO (last-in, first-out) order — the most recently registered defer runs first. This is the same ordering as Go’s defer.
Defer is per-function scope, not block-scoped. A defer registered inside a nested block, if, or while still runs at function exit, not at block exit.
;; Single defer — guaranteed cleanup(fn read_data ((path (ptr i8))) i64 (let (fd (call open_file path)) (block (defer (call close_file fd)) ;; runs when read_data exits (call process fd))))
;; Multiple defers — LIFO order;; Output: 1, 2, 3(fn lifo_example () void (block (defer (call print_i64 (i64 3))) ;; runs third (defer (call print_i64 (i64 2))) ;; runs second (defer (call print_i64 (i64 1)))) ;; runs first
;; Defer with explicit return(fn early_return ((x i64)) i64 (block (defer (call cleanup)) (if (lt x (i64 0)) (ret (i64 -1))) ;; cleanup runs before returning -1 x)) ;; cleanup also runs on normal exitRules:
deferhas typevoid.- The deferred expression is not evaluated when the
deferstatement is encountered — it is saved and executed later at function exit. - Deferred expressions have access to the variable bindings that were in scope at the point of the
deferstatement. - If a deferred expression itself causes an error, behavior is undefined.
6.22 Slice Operations
Section titled “6.22 Slice Operations”Slices are runtime-sized views into contiguous memory. They are represented as a fat pointer: {ptr data, u64 len} (16 bytes on 64-bit systems).
slice.of — Array to Slice
Section titled “slice.of — Array to Slice”(slice.of arr_expr)Converts a fixed-size array into a slice. The result is a (slice T) where T is the element type of the array. The slice borrows the array’s storage — the data pointer points into the original array.
(let (arr (arr.new i64 (i64 10) (i64 20) (i64 30))) (let (s (slice.of arr)) ;; s has type (slice i64), len=3 ...))slice.get — Bounds-Checked Read
Section titled “slice.get — Bounds-Checked Read”(slice.get s index_expr)Returns the element at the given index. The index must be an integer type. If the index is out of bounds (>= length), the program panics with "slice index out of bounds".
(slice.get s (i64 0)) ;; first element(slice.get s (i64 2)) ;; third elementslice.set — Bounds-Checked Write
Section titled “slice.set — Bounds-Checked Write”(slice.set s index_expr value_expr)Writes a value to the given index in the slice. The value type must match the slice’s element type. If the index is out of bounds, the program panics. slice.set has type void.
(slice.set s (i64 1) (i64 99)) ;; set s[1] = 99slice.len — Get Length
Section titled “slice.len — Get Length”(slice.len s)Returns the length of the slice as u64.
(let (n (slice.len s)) (call print_i64 (cast i64 n))) ;; print lengthRules:
slice.ofrequires an array operand. Passing a non-array type is a compile error.slice.getandslice.setrequire a(slice T)operand. Using them on other types is a compile error.slice.getreturnsT(the element type).slice.setreturnsvoid.slice.lenreturnsu64.- Index must be an integer type. Non-integer indices are a compile error.
slice.setvalue must match the element typeT. Type mismatch is a compile error.- All indexed access is bounds-checked at runtime. Out-of-bounds access calls
axon_panic. - Slices do not own their data — they borrow from the underlying array. The array must remain in scope while the slice is used.
6.23 Heap Allocation
Section titled “6.23 Heap Allocation”Axon provides built-in primitives for heap memory management. All allocations are zero-initialized and checked for out-of-memory conditions.
(alloc T) — Allocate Single Value
Section titled “(alloc T) — Allocate Single Value”Allocates space for one value of type T on the heap. Returns (ptr T).
(let (p (alloc i64)) ;; p : (ptr i64) (block (call print_i64 (deref p)) ;; reads zero-initialized value → 0 (free p))) ;; release memory(alloc.n T count) — Allocate Array
Section titled “(alloc.n T count) — Allocate Array”Allocates space for count values of type T on the heap. Returns (ptr T).
The count argument must be an integer expression.
(let (arr (alloc.n i64 (i64 100))) ;; arr : (ptr i64), 100 elements (block (call print_i64 (deref arr)) ;; first element is 0 (free arr)))(free ptr) — Deallocate Memory
Section titled “(free ptr) — Deallocate Memory”Frees heap-allocated memory. The argument must be a pointer type. Returns void.
(let (p (alloc i32)) (free p)) ;; releases the memoryRules:
allocandalloc.nreturn(ptr T)whereTis the specified element type.- Memory is zero-initialized (uses
callocinternally). - Allocation failure (out of memory) causes a runtime panic.
alloc.ncount must be a positive integer; zero or negative counts cause a runtime panic.freerequires a pointer operand. Passing a non-pointer is a compile error.freeaccepts null pointers safely (no-op).- It is the programmer’s responsibility to free allocated memory. Axon does not have garbage collection.
- Double-free and use-after-free are undefined behavior (as in C).
6.24 Lambda Expressions (Anonymous Functions)
Section titled “6.24 Lambda Expressions (Anonymous Functions)”Lambda expressions create anonymous functions inline. They evaluate to a function pointer value of type (fnptr ...) matching the lambda’s parameter and return types.
lambda ::= '(' 'fn' '(' param* ')' type expr ')'param ::= '(' IDENT type ')';; Simple identity lambda(fn ((x i64)) i64 x)
;; Lambda with arithmetic body(fn ((a i64) (b i64)) i64 (add a b))
;; Lambda with no parameters(fn () i64 (i64 42))Usage patterns:
;; Assign to a variable(let (double (fn ((x i64)) i64 (mul x (i64 2)))) (call print_i64 (call double (i64 21)))) ;; prints 42
;; Pass directly to a higher-order function(call apply (fn ((x i64)) i64 (mul x (i64 3))) (i64 5))
;; Store and reuse(let (sq (fn ((x i64)) i64 (mul x x))) (block (call print_i64 (call sq (i64 7))) ;; prints 49 (call print_i64 (call apply sq (i64 3)))));; prints 9Rules:
- Lambda syntax reuses the
fnkeyword but without a name —(fn (...params) ret body). - The body expression’s type must match the declared return type.
- Lambdas can capture variables from the enclosing scope. Captured variables are copied by value into a heap-allocated environment struct at closure creation time.
- The lambda is internally lifted to a top-level synthetic function (e.g.,
__lambda_0). Closures receive a hidden__envpointer parameter. This is an implementation detail. - A lambda expression has type
(fnptr (P1 P2 ...) R)matching its parameter and return types. - Lambdas can be used anywhere a function pointer is expected:
letbindings, function arguments, and data structures.
Closure example:
(let (x (i64 42)) (let (f (fn ((y i64)) i64 (add x y))) ;; captures x (call f (i64 10)))) ;; returns 527. Declarations
Section titled “7. Declarations”7.1 Function Declaration
Section titled “7.1 Function Declaration”(fn name ((param1 type1) (param2 type2)) return_type body_expr)Functions are the primary unit of code. Every function has:
- A name (must be unique within the module)
- Zero or more typed parameters
- A return type
- A body expression
The body expression’s type must match the declared return type.
Examples
Section titled “Examples”;; No parameters, returns i32(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(fn greet ((name (ptr i8))) void (call puts name))7.2 Extern Declaration
Section titled “7.2 Extern Declaration”(extern name ((param1 type1) ...) return_type)(extern name ((param1 type1) ... ...) return_type) ;; variadicExtern declarations import functions from C or the runtime. They have no body.
;; Import C's puts function(extern puts ((s (ptr i8))) i32)
;; Import C's printf (variadic)(extern printf ((fmt (ptr i8)) ...) i32)7.3 Struct Declaration
Section titled “7.3 Struct Declaration”(struct name ((field1 type1) (field2 type2) ...))Defines a named struct type with ordered, typed fields.
(struct Point ((x f64) (y f64)))(struct Rect ((origin Point) (width f64) (height f64)))7.4 Constant Declaration
Section titled “7.4 Constant Declaration”(const name type value_expr)Defines a compile-time constant.
(const PI f64 (f64 3.14159265358979))(const MAX_SIZE u32 (u32 1024))7.6 Test Declaration
Section titled “7.6 Test Declaration”(test "test name" body_expr)Test declarations define unit tests that are compiled and executed only when using the axonc test subcommand. Test files must NOT contain a main function — a synthetic main is generated.
name: A string literal naming the test (used in output).body_expr: A single expression. Use(block ...)for multiple expressions.
Tests are isolated: if a test panics (e.g., from a failed assertion), the test is marked as failed and execution continues with the next test. A summary is printed at the end.
(module (test "addition" (assert_eq (add (i64 1) (i64 2)) (i64 3))) (test "multiple checks" (block (assert_eq (i32 1) (i32 1)) (assert_ne (i64 0) (i64 1)))))7.7 Assertion Expressions
Section titled “7.7 Assertion Expressions”assert_eq / assert_ne
Section titled “assert_eq / assert_ne”(assert_eq left right) ;; panics if left ≠ right(assert_eq left right "message") ;; panics with custom message(assert_ne left right) ;; panics if left = right(assert_ne left right "message")Type constraints:
- Both
leftandrightmust have the same type. - Supported types:
i32,i64,u64,bool. - Floats and strings are not yet supported.
Runtime behavior:
- On success: no output, execution continues.
- On failure: prints a diagnostic (showing both values) and panics.
- When used inside a
(test ...)block, the panic is caught and the test is marked as failed.
8. Module System
Section titled “8. Module System”An AXON module is a single .axs file containing a (module ...) form. The module contains a sequence of declarations. Declaration order within a module does not matter — forward references are allowed (a function can call another function declared later in the module).
Every executable module must contain a main function with signature () → i32. The return value is the process exit code.
8.1 Imports
Section titled “8.1 Imports”AXON supports two forms of cross-module imports: namespaced imports and selective imports.
Namespaced Import — (import alias "path")
Section titled “Namespaced Import — (import alias "path")”Imports an entire module file under a namespace alias. Functions from the imported module are accessed via alias.function_name dot notation.
import_decl ::= '(' 'import' IDENT STRING ')'(module (import math "math.axs")
(fn main () i32 (block ;; Call sqrt from the math module via qualified access (call print_f64 (call math.sqrt (f64 2.0))) (i32 0))))The alias (math in this example) is a namespace identifier. It must be a valid AXON identifier. The string argument is the file path to the imported module.
Selective Import — (use "path" (names...))
Section titled “Selective Import — (use "path" (names...))”Imports specific functions from a module directly into the current module’s scope. No namespace qualifier is needed to call them.
use_decl ::= '(' 'use' STRING '(' IDENT* ')' ')'(module (use "utils.axs" (helper_fn other_fn))
(fn main () i32 (block ;; Call helper_fn directly — no qualifier needed (call print_i64 (call helper_fn (i64 42))) (i32 0))))All names listed in the (use ...) form must be pub functions in the target module.
8.2 Visibility — pub
Section titled “8.2 Visibility — pub”By default, all functions in a module are private — they are only accessible within the module that defines them.
To make a function accessible to other modules (via import or use), wrap the function declaration in a (pub ...) form:
;; math.axs — a library module(module ;; Public: accessible from other modules (pub (fn sqrt ((x f64)) f64 ;; implementation x))
;; Private: only accessible within this module (fn newton_step ((x f64) (guess f64)) f64 (div (add guess (div x guess)) (f64 2.0))))Attempting to access a private function from another module produces a compile error:
error: function 'newton_step' is private in module 'math'Visibility rules:
- Only
(pub (fn ...))functions are accessible cross-module. - Private functions (without
pub) cannot be called via qualified access (alias.fn) or imported via(use ...). - The
mainfunction does not need to bepub— it is the entry point, not an importable symbol.
8.3 Path Resolution
Section titled “8.3 Path Resolution”Import paths are resolved relative to the importing file’s directory.
project/├── main.axs ;; (import math "lib/math.axs") → resolves to project/lib/math.axs└── lib/ ├── math.axs ;; (import utils "utils.axs") → resolves to project/lib/utils.axs └── utils.axsImport paths must be string literals. Dynamic or computed paths are not supported.
8.4 Recursive Loading
Section titled “8.4 Recursive Loading”The compiler automatically loads imported files and their transitive imports. If a.axs imports b.axs, and b.axs imports c.axs, the compiler loads all three.
Circular imports are detected and handled — the compiler will not enter an infinite loop.
8.5 CLI Multi-File Compilation
Section titled “8.5 CLI Multi-File Compilation”Files can also be compiled together by passing multiple files on the command line:
axonc a.axs b.axs -o programIn this mode, all files share a flat namespace — no import or use declarations are needed, and all functions are visible to all files. This is independent of the module import system.
8.6 Complete Example — Multi-File Program
Section titled “8.6 Complete Example — Multi-File Program”math.axs — a library module:
(module (pub (fn square ((x i64)) i64 (mul x x)))
(pub (fn cube ((x i64)) i64 (mul x (mul x x)))))main.axs — the executable module:
(module ;; Namespaced import (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))))Alternatively, with selective import:
(module ;; Selective import — bring square directly into scope (use "math.axs" (square))
(fn main () i32 (block (call print_i64 (call square (i64 5))) ;; prints 25 (i32 0))))9. Operator Reference
Section titled “9. Operator Reference”9.1 Binary Operators
Section titled “9.1 Binary Operators”| Operator | Syntax | Operand Types | Result Type | Description |
|---|---|---|---|---|
add | (add a b) | integer, float | same as operands | Addition |
sub | (sub a b) | integer, float | same as operands | Subtraction |
mul | (mul a b) | integer, float | same as operands | Multiplication |
div | (div a b) | integer, float | same as operands | Division |
mod | (mod a b) | integer | same as operands | Modulo (remainder) |
eq | (eq a b) | any comparable | bool | Equal |
ne | (ne a b) | any comparable | bool | Not equal |
lt | (lt a b) | integer, float | bool | Less than |
le | (le a b) | integer, float | bool | Less than or equal |
gt | (gt a b) | integer, float | bool | Greater than |
ge | (ge a b) | integer, float | bool | Greater than or equal |
band | (band a b) | integer | same as operands | Bitwise AND |
bor | (bor a b) | integer | same as operands | Bitwise OR |
bxor | (bxor a b) | integer | same as operands | Bitwise XOR |
shl | (shl a b) | integer | same as operands | Shift left |
shr | (shr a b) | integer | same as operands | Shift right (arithmetic for signed) |
land | (land a b) | bool | bool | Logical AND (short-circuit) |
lor | (lor a b) | bool | bool | Logical OR (short-circuit) |
9.2 Unary Operators
Section titled “9.2 Unary Operators”| Operator | Syntax | Operand Type | Result Type | Description |
|---|---|---|---|---|
neg | (neg x) | integer, float | same as operand | Arithmetic negation |
not | (not x) | bool | bool | Logical NOT |
bnot | (bnot x) | integer | same as operand | Bitwise NOT |
9.3 Integer Overflow Detection
Section titled “9.3 Integer Overflow Detection”In debug builds (compiled with -fdebug-checks or AXON_DEBUG), signed integer add, sub, and mul operations are checked for overflow at runtime. If an overflow occurs, the program panics with a diagnostic message:
AXON PANIC: integer overflow: addIn release builds (compiled with -fno-debug-checks), integer arithmetic uses wrapping semantics with zero overhead.
| Build Mode | Behavior on Overflow | Overhead |
|---|---|---|
Debug (-fdebug-checks) | Panic with diagnostic | Function call per op |
Release (-fno-debug-checks) | Wrapping (two’s complement) | None |
Overflow checks apply to signed integer types only (i8, i16, i32, i64). Unsigned types always use wrapping semantics.
10. Built-in Functions
Section titled “10. Built-in Functions”The AXON runtime (libaxon_rt.a) provides the following built-in functions. They do not need extern declarations — they are always available.
| Function | Signature | Description |
|---|---|---|
print_i64 | (i64) → void | Print a 64-bit integer to stdout, followed by newline |
print_f64 | (f64) → void | Print a 64-bit float to stdout, followed by newline |
print_str | ((ptr i8)) → void | Print a null-terminated string to stdout, followed by newline |
print_bool | (bool) → void | Print true or false to stdout, followed by newline |
11. Memory Model
Section titled “11. Memory Model”11.1 Phase 1: Arena-Based
Section titled “11.1 Phase 1: Arena-Based”In Phase 1, all memory management is arena-based:
- Stack variables: Function parameters and
letbindings are stack-allocated (viaallocain the IR). They are automatically freed when the function returns. - String literals: Stored in the data section of the binary. They have static lifetime.
- No heap allocation: Phase 1 does not provide
malloc-like heap allocation. All data lives on the stack or in static storage. - No garbage collector: There is nothing to collect.
The compiler itself uses arena allocation internally (1 MiB blocks, pointer-bump allocation). This is an implementation detail and does not affect the source language semantics.
11.2 Phase 2: Regions + Linear Types
Section titled “11.2 Phase 2: Regions + Linear Types”Phase 2 introduces region-based memory management with linear type enforcement:
- Regions: Named memory regions with explicit lifetime scopes
- Arenas: Bulk-allocate, bulk-free memory pools
- Linear types: The type system enforces that resources are used exactly once, preventing leaks and use-after-free at compile time
- Stack allocation: The compiler may promote small allocations to the stack
12. Error Handling (Phase 2)
Section titled “12. Error Handling (Phase 2)”Phase 2 introduces result types for error handling:
;; Phase 2 syntax (not yet implemented)(fn read_file ((path (ptr i8))) (result (slice u8) IOError) ...)
(fn main () i32 (match (call read_file (str "data.txt")) (ok data (block (call process data) (i32 0))) (err e (block (call print_str (call error_msg e)) (i32 1)))))13. Concurrency Model (Phase 2)
Section titled “13. Concurrency Model (Phase 2)”Phase 2 introduces structured concurrency with actor-based message passing:
;; Phase 2 syntax (not yet implemented)(fn main () i32 (let (h (spawn compute_task (i64 42))) (let (result (join h)) (block (call print_i64 result) (i32 0)))))Key properties:
- Structured: spawned tasks cannot outlive their parent scope
- No shared mutable state: actors communicate via message passing
- Linear channels: send/receive channels enforce exactly-once delivery at compile time