Skip to content

Structs

Declare nominal product types with fields, constructors, methods, generics, and recursion.

Updated View as Markdown

A struct is a named product type. Struct names use PascalCase; field names use snake_case.

struct Point {
    x: float
    y: float
}

fn main(): void {
    origin := Point { x: 0.0, y: 0.0 }
    println("(${origin.x}, ${origin.y})")
}

Fields may be separated by newlines or commas, and a single-line declaration is often clearer for small types:

struct Point { x: float, y: float }

Construction

Construct a value by naming its fields. Named fields keep construction independent of declaration order, and commas are required between fields in a literal even when the declaration used newlines.

struct Point { x: float, y: float }

fn build(): Point {
    x := 10.0
    y := 20.0
    return Point { x, y }
}

fn main(): void {
    p := build()
    println("${p.x} ${p.y}")
}

The shorthand Point { x, y } uses visible bindings with the same names.

Field initializers run in source order. When constructing one field can fail, suspend, or call user code, lift that work into named bindings first so the order is explicit:

struct Order { customer: int, total: float }

error OrderError { NotFound }

fn load_customer(id: int): int ! OrderError { error NotFound }
fn calculate_total(id: int): float { return 0.0 }

fn build_order(customer_id: int): Order ! OrderError {
    customer := load_customer(customer_id)?
    total := calculate_total(customer_id)
    return Order { customer, total }
}

Initialize every field

The parser accepts field defaults, and the checker currently accepts a literal that omits fields, names unknown fields, or duplicates a field. None of that is diagnosed:

struct Retry {
    attempts: int = 3
    enabled: bool
}

An incomplete literal such as Retry { enabled: true } passes atoll check, then fails later in the pipeline — MIR validation rejects the function for reading an undefined local, and no WebAssembly is produced. The declared default is not applied.

Treat an incomplete literal as unsupported even though the current compiler accepts it. Initialize every declared field exactly once, and put reusable defaults in a named constructor:

struct Retry {
    attempts: int
    enabled: bool

    fn standard(): Retry => Retry { attempts: 3, enabled: true }
}

fn f(): int {
    return Retry.standard().attempts
}

fn main(): void {
    println("${f()}")
}

Access and mutation

Use . to read a field. Assigning one requires a mutable path to the value.

struct Point { x: float, y: float }

fn move_right(): float {
    mut p := Point { x: 0.0, y: 0.0 }
    p.x = 4.0
    return p.x
}

fn main(): void {
    println("${move_right()}")
}

The field declaration does not need a mut modifier — mutability comes from the binding and the receiver path. Nested assignment requires the whole path to be mutable; a mutable outer binding does not bypass a borrowed receiver’s contract, and assigning one field does not reconstruct the others.

Methods

Methods can be declared directly in the struct body.

struct Counter {
    value: int

    fn current(self): int => self.value

    fn increment(mut self): void {
        self.value += 1
    }
}

fn run(): int {
    mut c := Counter { value: 0 }
    c.increment()
    c.increment()
    return c.current()
}

fn main(): void {
    println("${run()}")
}

Receiver form is part of the method contract:

Receiver Capability
no self static function called through the type
self read the value and call non-mutating behavior
mut self update fields through the receiver

A function without self is static and is called on the type:

struct Point {
    x: float
    y: float

    fn zero(): Point => Point { x: 0.0, y: 0.0 }
}

fn f(): float {
    return Point.zero().x
}

fn main(): void {
    println("${f()}")
}

The same methods can be supplied from an inherent impl block:

struct Point { x: float, y: float }

impl Point {
    fn magnitude(self): float => (self.x * self.x + self.y * self.y).sqrt()
}

fn f(): float {
    return Point { x: 3.0, y: 4.0 }.magnitude()
}

fn main(): void {
    println("${f()}")
}

…or by a receiver-prefix function, which is convenient for extending a type from the module that needs the behavior:

struct Point { x: float, y: float }

fn Point.scaled(self, k: float): Point => Point { x: self.x * k, y: self.y * k }

fn f(): float {
    return Point { x: 1.0, y: 2.0 }.scaled(2.0).x
}

fn main(): void {
    println("${f()}")
}

Call syntax stays value.method() or Type.function() regardless of where the implementation is written. Methods do not change field visibility: a public method can expose a private field’s value, but external code still cannot name that field. Trait implementations are covered in Implementations.

Generics

Generic parameters use square brackets.

struct Page[T] {
    items: []T
    next_cursor: string?
}

fn f(): int {
    p := Page { items: [1, 2, 3], next_cursor: None }
    return p.items.len()
}

fn main(): void {
    println("${f()}")
}

Every concrete Page[T] has its fields checked with the substituted T. Inference can obtain T from the constructor fields or from an expected type; an empty collection field may still need an annotation when nothing constrains its element type.

Recursion

A recursive value needs a representational break — a managed container, an optional, or another supported indirection.

struct Node {
    value: int
    children: []Node
}

fn leaf(v: int): Node {
    return Node { value: v, children: [] }
}

fn main(): void {
    println("${leaf(1).value}")
}

A struct that embeds itself directly at full extent has no finite size and is rejected. Mutually recursive declarations are checked by the same rule.

Decorators

Struct and field decorators attach compiler or subsystem metadata.

struct Node {
    value: int
    children: []Node

    @weak
    parent: Node?
}

fn main(): void {
    root := Node { value: 1, children: [], parent: None }
    println("${root.value}")
}

@weak marks a non-owning field that can become None, which is how a cyclic shape such as a parent pointer avoids keeping its owner alive. The compiler requires a strong back-edge for it: a @weak field whose type has no owning path back to the declaring type is rejected with ATOLL3222: @weak field has no strong back-edge. Above, children is that strong edge.

@derive generates trait implementations on demand. Target restrictions differ per decorator — see Decorators.

Identity

Structs are nominal: two declarations with identical fields remain different types.

struct Width { value: int }
struct Height { value: int }

fn area(w: Width, h: Height): int {
    return w.value * h.value
}

fn main(): void {
    println("${area(Width { value: 2 }, Height { value: 3 })}")
}

The values do not interchange merely because both wrap one int. Nominality is module-aware: two modules can declare the same name and field list without creating one shared type, and imports retain the defining module’s identity.

Storage representation is compiler-selected. Source equality follows Equatable, not storage-handle identity.

Field ownership

A struct owns its fields according to their value kinds:

  • scalar fields are stored as scalar value lanes;
  • managed fields participate in compiler-inserted retain/release behavior;
  • safe reference fields retain or pin the appropriate owner when required;
  • @weak fields are non-owning and can become None;
  • raw pointer fields remain subject to unsafe and suspension validation.

Copying, returning, or storing a struct applies those rules recursively. Source code never destroys fields by hand, and adding a managed field can change the representation without changing the nominal source identity.

Spelling

The current declaration is struct Name { ... }. Legacy type Name { ... } and data Name { ... } are not current struct syntax — type declares an alias.

Use an anonymous record for a short-lived structural shape, and a model only when the fields describe a persisted database row.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close