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;
@weakfields are non-owning and can becomeNone;- raw pointer fields remain subject to
unsafeand 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.