Skip to content

Variables

Ard uses two keywords for variable declaration:

  • let for immutable bindings
  • mut for mutable bindings

Variable types can be inferred from their initial values:

let count = 42
let pi = 3.14
let greeting = "Hello"
let active = true

Types can be optionally be declared:

let name: Str = "Bob"
let temperature: Float = 98.6
let items: [Int] = [1, 2, 3]
let map: [Str:Int] = ["a": 1, "b": 2]

An immutable binding is read-only meaning it can neither be reassigned or mutated:

let x = 10
x = 20 // Error: cannot reassign immutable variable
let numbers = [1, 2, 3]
numbers.push(4) // Error: cannot mutate immutable variable

Variables declared with mut can be modified:

mut counter = 0
counter = 5 // OK
counter =+ 1 // OK, increment by 1

Ard uses a unique syntax for compound assignment operators, placing the = first for left-to-right readability:

mut value = 10
value =+ 5 // Equivalent to value = value + 5
value =- 2 // Equivalent to value = value - 2

There are no ++ or -- operators in Ard and only increment (=+) and decrement (=-) are supported.

Variables follow standard block scoping rules:

let outer = "global"
if some_condition {
let inner = "local"
// Both 'outer' and 'inner' are accessible here
}
// Only 'outer' is accessible here
// 'inner' is out of scope

A mut binding creates mutable local storage. In type positions, mut T means mutable reference to a T.

A function parameter marked mut receives mutable access to caller-owned storage, so the caller must pass an addressable mutable value. There is no extra mut marker at the call site:

struct Person { name: Str, age: Int }
fn update_person(mut person: Person) {
person.age = 99 // Mutates the caller's value
}
mut alice = Person { name: "Alice", age: 30 }
update_person(alice)
// alice.age is now 99

Passing an immutable value to a mutable parameter is a compile-time error:

let bob = Person { name: "Bob", age: 30 }
update_person(bob) // Error: expected a mutable Person

Mutable references may alias. If two mut T references point at the same mutable storage, mutations through either reference are visible through the other.

Struct fields can hold mutable references:

struct Context {
tree: mut ViewTree,
}
let ctx = Context{tree: tree}
ctx.tree.add_child(child)

The ctx binding is immutable, but ctx.tree is mutable access to the referenced ViewTree. Field assignment writes through the reference; it does not rebind the field slot.

mut T is also a representation boundary for recursive types, so it can be used to model linked structures and retained object graphs that require identity.

Redeclaring a variable with the same name in the same scope is allowed. This acts as a wipe of the binding and is only valid if their usages are consisten.

let x = 5
let x = x + 1 // Creates new variable, x is now 6
let x: Str = "hello" // Creates new variable with different type
x.size() // x is now a string and can only be used as a string