Structs
Structs group related data together into custom types.
Defining structs
struct Point {
x:f64
y:f64
}
struct Person {
name:str
age:i64
}
Creating instances
Use the struct name with field initializers:
struct Point {
x:f64
y:f64
}
fn main() {
Point {
x = 10.0
y = 20.0
} -> p
p <<x print nl // 10
p <<y print nl // 20
}
Reading fields
Use <<fieldname to read:
struct Rectangle {
width:f64
height:f64
}
fn area(rect:Rectangle -- a:f64) {
rect <<width rect <<height *
}
fn main() {
Rectangle {
width = 5.0
height = 3.0
} -> rect
rect area print nl // 15
}
Type narrowing with as
When working with raw ptr values (common in callbacks), use as to tell the compiler the struct type:
fn handler(data:ptr -- ) {
data as Config <<debug print nl
}
This is compile-time only — no runtime cost. It's required when multiple structs share a field name, as the compiler cannot guess which one you mean.
Writing fields
Use >>fieldname to write. Two variants:
>>field— pushes modified struct back (for chaining)>>field!— discards struct (for standalone mutation)
struct Counter {
value:i64
}
fn main() {
Counter {
value = 0
} -> c
c <<value print nl // 0
// >>field! for standalone mutation
c 10 >>value!
c <<value print nl // 10
// >>field with rebind
c c <<value inc >>value -> c
c <<value print nl // 11
}
Chain writes during construction:
Point { x = 0.0 y = 0.0 } 1.0 >>x 2.0 >>y -> p
Nested structs
Structs can contain other structs:
struct Point {
x:f64
y:f64
}
struct Line {
start:Point
end:Point
}
fn main() {
Point {
x = 0.0
y = 0.0
} -> p1
Point {
x = 10.0
y = 10.0
} -> p2
Line {
start = p1
end = p2
} -> line
line <<start <<x print nl // 0
line <<end <<x print nl // 10
}
Structs with arrays
struct Point {
x:f64
y:f64
}
struct Polygon {
points:[]Point
}
fn main() {
3 make<ptr> -> pts
Point {
x = 0.0
y = 0.0
} -> p0
Point {
x = 1.0
y = 0.0
} -> p1
Point {
x = 0.5
y = 1.0
} -> p2
pts 0 p0 set
pts 1 p1 set
pts 2 p2 set
Polygon {
points = pts
} -> triangle
triangle <<points len print nl // 3
}
Struct methods
Methods are functions that operate on a specific struct type. Define them using receiver syntax:
use math
struct Point {
x:f64
y:f64
}
// Method with receiver syntax: fn (receiver:Type) name(params -- returns)
fn (p:Point) magnitude( -- m:f64) {
p <<x dup * p <<y dup * + math::sqrt
}
fn (p:Point) move(dx:f64 dy:f64 -- ) {
p p <<x dx + >>x -> p
p p <<y dy + >>y -> p
}
fn main() {
Point { x = 3.0 y = 4.0 } -> pt
// Call method on struct - receiver is popped from stack
pt magnitude print nl // 5
// Methods can also work on inline struct construction
Point { x = 5.0 y = 12.0 } magnitude print nl // 13
// Method with parameters
pt 1.0 1.0 move
pt <<x print nl // 4
}
Method resolution
When a struct is on the stack and you use an identifier, methods take precedence over global functions:
struct Counter {
value:i64
}
fn (c:Counter) bump( -- result:i64) {
c <<value 1 +
}
fn bump( -- result:i64) {
999
}
fn main() {
Counter { value = 10 } -> c
c bump print nl // 11 (calls method)
bump print nl // 999 (calls global function)
}
Traditional function style
You can also define functions that take structs as parameters:
use math
struct Point {
x:f64
y:f64
}
fn point_distance(p1:Point p2:Point -- d:f64) {
p2 <<x p1 <<x - dup *
p2 <<y p1 <<y - dup *
+ math::sqrt
}
fn main() {
Point { x = 0.0 y = 0.0 } -> a
Point { x = 3.0 y = 4.0 } -> b
a b point_distance print nl // 5
}
Common patterns
Builder pattern
struct Config {
debug:i64
verbose:i64
max_retries:i64
}
fn config_new( -- cfg:Config) {
Config {
debug = 0
verbose = 0
max_retries = 3
}
}
fn config_set_debug(cfg:Config value:i64 -- cfg:Config) {
cfg value >>debug -> cfg
cfg
}
fn main() {
config_new 1 config_set_debug -> cfg
cfg <<debug print nl // 1
}
Linked list node
struct Node {
value:i64
next:ptr
}
fn node_new(value:i64 -- node:ptr) {
Node {
value = value
next = 0
}
}
fn main() {
10 node_new -> first
20 node_new -> second
30 node_new -> third
first second >>next -> first
second third >>next -> second
// Traverse
first -> current
loop {
current 0 == if { break }
current <<value print nl
current <<next -> current
}
// Output: 10 20 30
}
Stack data structure
struct Stack {
data:ptr
top:i64
capacity:i64
}
fn stack_new(capacity:i64 -- s:Stack) {
capacity make<i64> -> data
Stack {
data = data
top = 0
capacity = capacity
}
}
fn stack_push(s:Stack value:i64 -- ) {
s <<data s <<top value set
s s <<top 1 + >>top -> s
}
fn stack_pop(s:Stack -- value:i64) {
s s <<top 1 - >>top -> s
s <<data s <<top nth
}
fn main() {
10 stack_new -> s
s 1 stack_push
s 2 stack_push
s 3 stack_push
s stack_pop print nl // 3
s stack_pop print nl // 2
s stack_pop print nl // 1
}
Struct equality
Compare structs field by field:
struct Point {
x:f64
y:f64
}
fn points_equal(a:Point b:Point -- equal:i64) {
a <<x b <<x == a <<y b <<y == and
}
fn main() {
Point {
x = 1.0
y = 2.0
} -> p1
Point {
x = 1.0
y = 2.0
} -> p2
Point {
x = 3.0
y = 4.0
} -> p3
p1 p2 points_equal print nl // 1 (true)
p1 p3 points_equal print nl // 0 (false)
}
Pointer fields and recursive structs
Structs can contain pointers to other structs using *StructName. This enables linked lists, trees, and other recursive data structures.
struct Node {
value:i64
next:*Node
}
fn main() {
// Build a linked list: 3 -> 2 -> 1 -> null
Node { value = 1 next = null } -> a
Node { value = 2 next = a } -> b
Node { value = 3 next = b } -> c
// Traverse using <<field access through pointers
c <<value print nl // 3
c <<next <<value print nl // 2
}
Use null for empty pointer fields. Check for null before accessing:
fn print_list(cur:ptr -- ) {
loop {
cur null eq if { break }
cur <<value print nl
cur <<next -> cur
}
}
Binary tree example
struct TreeNode {
value:i64
left:*TreeNode
right:*TreeNode
}
fn inorder(n:ptr -- ) {
n null neq if {
n <<left inorder
n <<value print nl
n <<right inorder
}
}
What's next?
Learn about Constants & Enums to define fixed values.