Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

You are reading the development version (master). For the latest release (v3.5.0) see the stable docs.

.ax Language Reference

Looking for the formal specification? This page is the narrative, example-driven reference. The authoritative grammar, type rules, and capability semantics live in docs/spec/ax-language-1.0.md. When the two disagree, the spec wins.

.ax is Boruna’s statically-typed, deterministic scripting language. It compiles to Boruna bytecode and runs on the Boruna VM. It is designed for workflow steps: small, focused, pure functions with explicit capability declarations for any side effects.

File structure

Every standalone .ax file must define fn main() -> Int. The main function is the entry point for boruna run.

fn main() -> Int {
    42
}

Types

TypeDescriptionExample
Int64-bit signed integer42, -7
Float64-bit float3.14
StringUTF-8 string"hello"
BoolBooleantrue, false
UnitNo value()
Option<T>Optional valueSome(42), None
Result<T, E>Success or errorOk(42), Err("msg")
List<T>Ordered list[1, 2, 3]
Map<K, V>Key-value map{"a": 1, "b": 2}
RecordsNamed fieldsPoint { x: 1, y: 2 }
EnumsTagged unionShape::Circle(5)

Variables

Use let with an explicit type annotation:

let name: String = "Boruna"
let count: Int = 0
let flag: Bool = true

A binding that you want to change later is declared with let mut and rebound with =:

let mut total: Int = 0
total = total + 5

Rebinding changes what the name refers to; values themselves (records, lists, maps) are never modified in place. Reassigning a binding declared without mut still compiles, but boruna lang check reports warning E010 and boruna lang repair adds the missing mut. It will be a compile error in language version 2.0.

Loops

fn sum(items: List<Int>) -> Int {
    let mut total: Int = 0
    for x in items {
        total = total + x
    }
    total
}

fn factorial(n: Int) -> Int {
    let mut result: Int = 1
    let mut i: Int = n
    while i > 0 {
        result = result * i
        i = i - 1
    }
    result
}

for iterates a List in order; the loop variable and any let inside the body are scoped to the body. A loop that never ends is stopped by the step limit (--step-limit) with a runtime error. Recursion still works and is often the clearer choice.

No semicolons. Each statement is on its own line.

Functions

fn add(a: Int, b: Int) -> Int {
    a + b
}

The last expression in a function body is the return value. No return keyword needed.

Capability annotations

Functions that perform side effects must declare the required capabilities:

fn fetch(url: String) -> String !{net.fetch} {
    // live implementation
}

fn call_model(prompt: String) -> String !{llm.call} {
    // live implementation
}

// Multiple capabilities
fn fetch_and_cache(url: String) -> String !{net.fetch, fs.write} {
    // live implementation
}

Without the annotation, the VM will reject any attempt to call the capability at runtime.

Intent declarations

A function may declare a single machine-read purpose with an intent "..." clause after its signature. The clause is optional and order-independent with capability and contract clauses:

fn transfer(amount: Int) -> Int !{db.write} intent "Move funds between accounts" {
    // implementation
}

Intent is captured into the run’s evidence bundle (intents.json, keyed by step id) so an auditor sees what each step was authorized to do alongside what it actually did. It is covered by the bundle checksums, so tampering with a captured intent makes boruna evidence verify fail. A function may declare at most one intent; a second is a compile error.

Contracts (requires)

A function may declare one or more requires <expr> preconditions, checked at runtime against its arguments on entry:

fn transfer(amount: Int) -> Int !{db.write} requires amount > 0 {
    // runs only if amount > 0
}

If a precondition is false when the function is called, execution traps with a contract violation carrying a counterexample — the concrete arguments that triggered it (e.g. [0]) — so the failing input is reproducible. In a workflow, the violation surfaces with the stable error_kind contract_violation and the counterexample is recorded in the run’s hash-chained audit log (tamper-evident evidence). A violation is deterministic in the inputs, so it is not retry-eligible.

Contracts are enforced purely at runtime (concrete-trace checking) — Boruna does not use SMT/symbolic proving. ensures postconditions are parsed but not yet enforced.

Records

Define named record types with the type keyword:

type Point {
    x: Int,
    y: Int,
}

let p: Point = Point { x: 3, y: 4 }
let px: Int = p.x

Record spread creates an updated copy:

let p2: Point = Point { ..p, y: 10 }

Enums

Each variant is either a unit variant or carries a single payload value. Construct a value with the EnumName::Variant(payload) form (unit variants take no parentheses):

enum Shape {
    Circle(Float),
    Square(Float),
}

let s: Shape = Shape::Circle(5.0)

Pattern matching

let result: String = match s {
    Circle(radius) => "circle"
    Square(side) => "square"
    _ => "unknown"
}

Match on Option:

let value: Option<Int> = Some(42)
let n: Int = match value {
    Some(x) => x
    None => 0
}

Match on Result:

let r: Result<Int, String> = Ok(99)
let out: Int = match r {
    Ok(v) => v
    Err(_) => -1
}

Match on strings:

let greeting: String = match lang {
    "en" => "hello"
    "es" => "hola"
    _ => "hi"
}

Conditionals

let label: String = if score > 90 {
    "pass"
} else {
    "fail"
}

Lists

let items: List<Int> = [1, 2, 3, 4, 5]

List operations are available through the standard library.

Maps

let config: Map<String, Int> = { "timeout": 30, "retries": 3 }

Framework apps

Framework apps implement the Elm architecture. They must define:

fn init() -> State { ... }
fn update(state: State, msg: Msg) -> UpdateResult { ... }
fn view(state: State) -> UINode { ... }

Where State, Msg, Effect, UpdateResult, UINode, and PolicySet are the framework protocol types. See FRAMEWORK_SPEC.md for the full protocol.

Syntax quick reference

// Comments use double-slash

// Variables (type required; add `mut` to rebind later)
let x: Int = 42
let mut n: Int = 0
n = n + 1

// Loops
for item in [1, 2, 3] { n = n + item }
while n > 0 { n = n - 1 }

// Function
fn square(n: Int) -> Int {
    n * n
}

// Capability function
fn now() -> Int !{time.now} {
    // implementation
}

// Record literal
Point { x: 1, y: 2 }

// Record spread
Point { ..point, x: 10 }

// Enum variant
Shape::Circle(5.0)

// Pattern match
match x {
    0 => "zero"
    _ => "nonzero"
}

// Option
Some(42)
None

// Result
Ok("value")
Err("message")

// List
[1, 2, 3]

// Map
{ "key": "value" }

Import statements

import "std-name"

Import statements load a standard library package at compile time. The library source is inlined into the compilation unit before type-checking. The import line itself is removed from the compiled output.

Standard library packages are resolved from the libs/ directory relative to the current working directory. When a library source is inlined, any fn main() -> Int stub present in the library file is stripped so it does not conflict with the importing program’s own main.

Example:

import "std-json"

fn main() -> Int {
    let s: String = int_to_string(42)
    0
}

Built-in functions

These functions are provided by the runtime and do not need to be imported:

FunctionSignatureDescription
__builtin_int_to_string(Int) -> StringConvert an integer to its decimal string representation
__builtin_float_to_string(Float) -> StringConvert a float to its string representation
__builtin_string_len(String) -> IntLength of a string in bytes
__builtin_string_chars(String) -> List<String>Split a string into a list of single-character strings
__builtin_string_contains(String, String) -> BoolTrue if first string contains the second
__builtin_string_starts_with(String, String) -> BoolTrue if string starts with prefix
__builtin_string_ends_with(String, String) -> BoolTrue if string ends with suffix
__builtin_string_to_upper(String) -> StringUppercase copy
__builtin_string_to_lower(String) -> StringLowercase copy
__builtin_string_trim(String) -> StringStrip leading/trailing whitespace
__builtin_string_join(List<String>, String) -> StringJoin list with separator
__builtin_string_split(String, String) -> List<String>Split string on a delimiter
__builtin_string_replace(String, String, String) -> StringReplace first occurrence of pattern
__builtin_string_slice(String, Int, Int) -> StringSubstring by byte offsets
__builtin_int_parse(String) -> Result<Int, String>Parse a decimal integer string
__builtin_float_parse(String) -> Result<Float, String>Parse a float string
__builtin_bool_to_string(Bool) -> StringConvert a bool to “true” or “false”
__builtin_list_len(List<T>) -> IntNumber of elements
__builtin_list_is_empty(List<T>) -> BoolTrue if list has zero elements
__builtin_list_head(List<T>) -> Option<T>First element, or None
__builtin_list_tail(List<T>) -> List<T>All elements after the first
__builtin_list_append(List<T>, T) -> List<T>New list with item added at end
__builtin_list_concat(List<T>, List<T>) -> List<T>Concatenate two lists
__builtin_list_reverse(List<T>) -> List<T>Reversed copy
__builtin_map_get(Map<String, V>, String) -> Option<V>Look up a key; returns Some(v) or None
__builtin_map_set(Map<String, V>, String, V) -> Map<String, V>Return a new map with key set to value
__builtin_map_remove(Map<String, V>, String) -> Map<String, V>Return a new map with key removed
__builtin_map_contains_key(Map<String, V>, String) -> BoolTrue if key is present
__builtin_map_keys(Map<String, V>) -> List<String>All keys in sorted order
__builtin_map_values(Map<String, V>) -> List<V>All values in key-sorted order
__builtin_map_len(Map<String, V>) -> IntNumber of entries

These built-ins are also wrapped in std-json (via int_to_string, json_escape) and can be called directly in any .ax file.

Note on naming: The __builtin_ prefix distinguishes these from user-defined functions and prevents shadowing. User-facing wrappers in stdlib packages use cleaner names.

What .ax is not

.ax is deliberately minimal. It does not have:

  • Mutable variables (use record spread for state transitions)
  • Loops (use recursion or standard library functions)
  • Exceptions (use Result<T, E>)
  • Implicit side effects (every effect must be declared)
  • Generics (types are concrete at definition time)

These omissions are intentional. They keep the language deterministic and auditable.