Variables
Declaration keywords
Section titled “Declaration keywords”Ard uses two declaration keywords:
letcreates a binding whose storage cannot be written.mutcreates a binding whose storage can be written: the whole value can be replaced, and its fields can be assigned.
let name = "Ada"// name = "Grace" // Error: the binding is immutable
mut count = 1count = 2count =+ 1Type inference and annotations
Section titled “Type inference and annotations”Types are normally inferred, but annotations are available:
let name: Str = "Bob"let temperature: Float64 = 98.6let items: [Int] = [1, 2, 3]let labels: [Str: Int] = ["a": 1, "b": 2]A pointer keeps its pointer type during inference:
struct User { name: Str }
mut user = User{name: "Ada"}let pointer = &mut user // inferred as &mut Userlet alias = pointer // also &mut User; copies the pointerMutable bindings
Section titled “Mutable bindings”A mut binding owns its storage, including the fields of a struct stored inline:
struct User { name: Str }
mut user = User{name: "Ada"}user.name = "Grace" // OK: writes user's own storageuser = User{name: "Lin"} // OK: replaces the whole value
let fixed = User{name: "Ada"}// fixed.name = "Grace" // Error: fixed is not writableWriting to a binding never affects other values. Copies of a struct are independent:
mut first = User{name: "Ada"}let second = firstfirst.name = "Grace" // second.name is still "Ada"Lists and maps are the exception: copies share storage, so in-place operations such as push and set need a pointer even on a mut binding. See Lists and maps.
Pointers
Section titled “Pointers”A pointer shares storage. Ard has two pointer types:
| Type | Created with | Meaning |
|---|---|---|
&T | &value | Read-only pointer: reads through the pointer only. |
&mut T | &mut value | Writable pointer: reads and writes through the pointer. |
struct User { name: Str }
mut user = User{name: "Ada"}
let reader = &user // &Userlet writer = &mut user // &mut User
writer.name = "Grace" // writes userreader.name // "Grace"Field access and method calls go through a pointer implicitly. A &mut T can be used wherever a &T is expected; the reverse is rejected.
& works on any addressable place. &mut needs a writable place: a mut binding, a field of one, or a place reached through a &mut pointer. A writable pointer can change or replace the value it points to, and a let value never changes, so &mut of a let binding is rejected. Rust’s let mut and Zig’s var follow the same rule. &mut of a fresh value, such as &mut User{...}, always works because it creates new storage:
let fixed = User{name: "Ada"}let a = &fixed // OK: read-only pointer// let b = &mut fixed // Error: fixed is not writable
let fresh = &mut User{name: "Lin"} // &mut User to new storage&T is read-only only through that pointer. Other &mut T pointers to the same storage can still write it.
A pointer binding declared with let cannot be rebound, but it can still write through to its pointee. A mut pointer binding can also point somewhere else:
mut first = User{name: "First"}mut second = User{name: "Second"}mut current = &mut firstlet alias = current
current = &mut second // rebinds only currentalias.name = "One" // still writes firstcurrent.name = "Two" // writes secondPointer parameters
Section titled “Pointer parameters”Parameters are immutable bindings. To let a function change the caller’s value, take a &mut T parameter:
fn rename(user: &mut User, name: Str) { user.name = name}
mut user = User{name: "Ada"}rename(&mut user, "Grace")A function that only needs its own writable copy shadows the parameter instead:
fn renamed(user: User, name: Str) User { mut user = user user.name = name user}Dereferencing with .*
Section titled “Dereferencing with .*”Postfix .* gives the value at a pointer. Reading it produces a shallow copy:
mut user = User{name: "Ada"}let pointer = &mut userlet snapshot: User = pointer.*pointer.* is also a place. Through a &mut T pointer it can be assigned, replacing the whole pointee:
pointer.* = User{name: "Grace"} // user is now Gracepointer.*.name = "Lin" // same as pointer.name = "Lin".* evaluates its operand once and composes left to right with calls and member access: load().*.name.
The copy is shallow:
- structs, fixed arrays, and primitive values copy their current value;
- pointer-valued fields keep copied pointers;
- lists initially share their existing backing storage, although later growth may detach one descriptor;
- maps continue sharing map contents;
- channels and foreign handles retain their intrinsic sharing behavior.
Ard does not provide a deep copy. Programs that need one construct it explicitly.
Pointers compare by identity. Compare pointee values explicitly when their types support equality:
let same_place = pointer == &user
mut count = 1let count_pointer = &mut countlet same_value = count_pointer.* == countLists and maps
Section titled “Lists and maps”Copying a list copies its descriptor and shares its backing storage. Copying a map shares its contents. To keep that sharing visible, in-place list and map operations such as push, set, swap, and delete require a &mut pointer. A mut binding can only replace the whole value:
mut values = [1, 2]values = [3] // OK: replaces the value// values.push(4) // Error: requires &mut [Int]
let items = &mut [1, 2]items.push(3) // OKPointer-valued fields
Section titled “Pointer-valued fields”Struct fields can store pointers. Writing through a pointer field targets the pointee, so it does not need the containing value to be writable:
struct Tree { value: Int }struct Context { tree: &mut Tree }
let context = Context{tree: &mut Tree{value: 1}}context.tree.value = 2 // OK: writes the Tree
// context.tree = &mut Tree{value: 3} // Error: writes context's own storageMigrating from mut references
Section titled “Migrating from mut references”Earlier releases spelled pointers with mut. That syntax still works but reports deprecation warnings, and it will be removed in a future release:
| Before | Now |
|---|---|
user: mut User | user: &mut User |
mut user | &mut user |
mut pointer (already a pointer) | pointer |
pointer.@ | pointer.* |
mut Trait is unchanged; see Traits.
ard migrate <path> rewrites the mechanical cases in place. Use ard migrate --check <path> to list files that still need migration without changing them. Borrowing a value parameter, for binding, or multi-statement match binding is rewritten by adding mut name = name at the start of its containing body. Single-statement match arms stay manual because they may be expression arms. For Go []T and map[K]V parameters, an immutable mut value is rewritten to value; writable operands remain explicit &mut value. Some uses still need a manual change and are reported instead, including captures of outer parameters, non-mut method receivers, trait-typed places, and Go *[]T or *map[K]V parameters.
Shadowing
Section titled “Shadowing”Redeclaring a name in the same scope creates a new binding:
let x = 5let x = x + 1let x: Str = "hello"x.size()