Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025

Structs

Structs let you create custom types by grouping related data together.

Defining Structs

struct Point {
    x: i32,
    y: i32,
}

fn main() -> i32 {
    let origin = Point { x: 0, y: 0 };
    let target = Point { x: 3, y: 4 };

    println(@to_string(origin.x));  // prints: 0
    println(@to_string(target.y));  // prints: 4

    0
}

Structs in Functions

Pass structs to functions and return them:

struct Point {
    x: i32,
    y: i32,
}

fn distance_squared(p1: Point, p2: Point) -> i32 {
    let dx = p2.x - p1.x;
    let dy = p2.y - p1.y;
    dx * dx + dy * dy
}

fn main() -> i32 {
    let origin = Point { x: 0, y: 0 };
    let target = Point { x: 3, y: 4 };

    let dist_sq = distance_squared(origin, target);
    println(@to_string(dist_sq));  // prints: 25 (distance is 5)

    dist_sq
}

Nested Structs

Structs can contain other structs:

struct Point {
    x: i32,
    y: i32,
}

struct Rectangle {
    origin: Point,
    width: i32,
    height: i32,
}

fn area(rect: Rectangle) -> i32 {
    rect.width * rect.height
}

fn origin_x(borrow rect: Rectangle) -> i32 {
    rect.origin.x
}

fn main() -> i32 {
    let rect = Rectangle {
        origin: Point { x: 10, y: 20 },
        width: 100,
        height: 50,
    };

    println(@to_string(origin_x(borrow rect)));  // prints: 10
    println(@to_string(area(rect)));             // prints: 5000

    0
}

Mutable Struct Fields

If a struct variable is mutable, you can modify its fields:

struct Counter {
    value: i32,
}

fn main() -> i32 {
    let mut c = Counter { value: 0 };
    c.value = c.value + 1;
    c.value = c.value + 1;

    println(@to_string(c.value));  // prints: 2
    c.value
}

Ownership: Moves and Copies

Integers and booleans are copied when you use them. Structs are different: unless you opt in to copying, a struct value is moved when it is assigned or passed by value. After a move, the old place no longer owns a live value.

struct Point {
    x: i32,
    y: i32,
}

fn use_point(p: Point) {
    println(@to_string(p.x));
}

fn main() -> i32 {
    let p = Point { x: 1, y: 2 };
    use_point(p);     // p moves here
    // use_point(p);  // ERROR: value already moved

    0
}

The same rule applies to assignment:

struct Point {
    x: i32,
    y: i32,
}

fn main() -> i32 {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;      // p1 moves into p2

    println(@to_string(p2.x));       // prints: 1
    // println(@to_string(p1.x));    // ERROR: p1 was moved

    0
}

If a struct is just data and duplicating it is safe, mark it with @copy. Copies leave the source usable:

@copy
struct Point {
    x: i32,
    y: i32,
}

fn main() -> i32 {
    let p1 = Point { x: 1, y: 2 };
    let mut p2 = p1;  // p2 is a copy; p1 is still valid

    p2.x = 100;

    println(@to_string(p1.x));  // prints: 1 (unchanged)
    println(@to_string(p2.x));  // prints: 100

    0
}

Use the default move behavior for values that represent ownership, resources, or anything that should not be duplicated accidentally. Use @copy only for small plain-data structs whose fields can all be copied.