Tzopilotl
Docs
GitHub

Tzopilotl by Example

7. Data Structures

7.1 Structs

struct Point {
    x Float,
    y Float
}

struct Person {
    name String,
    age Int
}

-- Construction
let p = Point { x: 3.0, y: 4.0 };
let alice = Person { name: "Alice", age: 30 };

-- Positional construction (fields assigned in declaration order)
let p2 = Point{3.0, 4.0};
p2.x println;           -- 3.0

-- Field access
p.x println;
alice.name println;

-- Nested structs
struct Line {
    start Point,
    end Point
}

let line = Line { start: p, end: Point { x: 6.0, y: 8.0 } };
line.start.x println;

7.2 Struct Update (Spread)

Create a new struct from an existing one, overriding specific fields with .... The spread source comes first, followed by the fields to override. The original struct is not mutated.

-- Start with p, override x
let p = Point { x: 1.0, y: 2.0 };
let p2 = Point { ...p, x: 10.0 };
p2.x println;           -- 10.0
p2.y println;           -- 2.0  (copied from p)

-- Copy all fields (clone)
let p3 = Point { ...p };

-- Start with fred, override two fields
struct Employee { name String, age Int, dept String }
let fred = Employee { name: "Fred", age: 30, dept: "Engineering" };
let fred2 = Employee { ...fred, age: 31, dept: "Management" };
fred2.age println;      -- 31
fred2.dept println;     -- Management
fred2.name println;     -- Fred  (unchanged)
fred.age println;       -- 30   (original unmodified)

-- Works with nested structs
let line2 = Line { ...line, start: Point { x: 1.0, y: 1.0 } };

-- Works with template structs
struct Pair<A, B> { first A, second B }
let pair1 = Pair { first: 1, second: "hello" };
let pair2 = Pair { ...pair1, first: 2 };
pair2.second println;   -- hello

-- Int-to-float promotion works with spread
let p4 = Point { ...p, x: 42 };
p4.x println;           -- 42.0

7.3 Tuple Structs

Tuple structs have positional fields accessed by numeric index. They are useful for newtypes and lightweight wrappers.

-- Tuple struct declaration (parentheses, semicolon terminated)
struct Point(Float, Float);
struct Temperature(Float);

-- Construction uses function-call syntax
let p = Point(3.0, 4.0);
let temp = Temperature(98.6);

-- Field access by numeric index
p.0 println;              -- 3.0
p.1 println;              -- 4.0
temp.0 println;           -- 98.6

-- Pattern matching on tuple structs
let Point(x, y) = p;
println(x + y);            -- 7.0

match (temp) {
    Temperature(v): v println;    -- 98.6
}

-- Printing
p println;                 -- Point(3.0, 4.0)
temp println;              -- Temperature(98.6)

7.4 Enums (Tagged Unions)

enum Shape {
    circle Float,
    rect (Float, Float),
    point,
}

enum Result {
    ok String,
    error String,
}

enum Value {
    integer Int,
    floating Float,
    text String,
    nothing,
}

-- Construction
let s = Shape.circle(5.0);
let p = Shape.point;
let ok = Result.ok("success");

-- Pattern match to extract values
fn area(s Shape) Float {
    match (s) {
        Shape.circle(r): return 3.14159 * r * r;
        Shape.rect(dims): return dims.0 * dims.1;
        Shape.point: return 0.0;
    }
    0.0
}

Built-in Option<T>

Option<T> is a built-in template enum with cases some T and none. It is returned by map subscript and get(map, key). See Maps and Templates for details.

Error propagation with postfix try

The postfix try operator unwraps a Result<T, E> (from std.result) or an Option<T>: on ok(v)/some(v) the expression evaluates to v; on err(e)/none it early-returns the error from the enclosing function. The function's declared return type must be a Result with the same error type (or an Option). Like await, try sits in the postfix layer, so it binds tighter than any binary operator and chains mid-pipeline.

import std.result.*;

fn parsePositive(s String) Result<Int, String> {
    match (parseInt(s)) {
        Option.some(n): n >= 0 ? Result<Int, String>.ok(n)
                               : Result<Int, String>.err("negative: " $ s);
        Option.none: Result<Int, String>.err("not a number: " $ s);
    }
}

fn addParsed(a String, b String) Result<Int, String> {
    let x = parsePositive(a) try;   -- err returns immediately
    let y = parsePositive(b) try;
    Result<Int, String>.ok(x + y)
}

-- chains mid-pipeline: unwrap, then keep piping
fn doubled(s String) Result<Int, String> {
    Result<Int, String>.ok(parsePositive(s) try * 2)
}

Inside a lambda, try returns from the lambda; inside an async fn, the early return resolves the function's Future. Mixing kinds (an Option-try in a Result-returning function), mismatched error types, and try in a function without a declared Result/Option return type are all compile-time errors.

7.5 Ordinal (Case Index)

-- ordinal returns the zero-based index of an enum case
enum Color { red, green, blue, }

ordinal(Color.red) println;      -- 0
ordinal(Color.green) println;    -- 1
ordinal(Color.blue) println;     -- 2

-- Works with data-carrying cases too
ordinal(Option.some(42)) println;  -- 0
ordinal(Option<Int>.none) println; -- 1

-- Works with template enums
enum Either<L, R> { left L, right R, }
ordinal(Either.left(42)) println;              -- 0
ordinal(Either<Int, String>.right("hi")) println; -- 1

7.6 Tag (Case Name)

-- tag returns the case name of an enum value as a Symbol
enum Color { red, green, blue, }

tag(Color.red) println;      -- 'red
tag(Color.green) println;    -- 'green
tag(Color.blue) println;     -- 'blue

-- Works with data-carrying cases too
tag(Option.some(42)) println;  -- 'some
tag(Option<Int>.none) println; -- 'none

-- Works with template enums
enum Either<L, R> { left L, right R, }
tag(Either.left(42)) println;              -- 'left
tag(Either<Int, String>.right("hi")) println; -- 'right