struct


Struct

A struct is a compound data type that groups multiple named fields together into a single unit. Each field has a name and a type. The fields are stored in memory in the order they are declared, and this order is guaranteed by the language. The conceptual layout and the physical layout are always the same.

    
Planet :: struct {
    name:           string;
    orbit_radius:   int;
    num_moons:      int;
    ring_count:     int;
}
    

Structs help you organize your program by grouping related pieces of data together. Instead of passing around many loose variables, you pass a single struct. You can write procedures that take a struct as an argument, assign one struct to another, and print an entire struct with print.

Declaring and Using Structs

    
Bounding_Box :: struct {
    min_x: float;
    min_y: float;
    max_x: float;
    max_y: float;
    label: string;
}

b: Bounding_Box;          // All members initialized to their default values (0, "").
b.min_x = 10;
b.min_y = 5;
b.max_x = 50;
b.max_y = 55;
b.label = "hitbox";

print("b is: %\n", b);
    

Compound Declarations

Multiple fields of the same type can be declared on a single line:

    
Color :: struct {
    r, g, b: float;
}
    

Default Values

Struct fields can have default values, using the same declaration syntax you would use for local variables:

    
Spaceship :: struct {
    fuel_level    := 85.0;
    model         := "falcon";
    num_engines   := 4;
    hull_type     := "titanium";
    has_shields   := false;
}

craft: Spaceship;  // Gets all the defaults.
print("craft is: %\n", craft);
    

You can also override default values of sub-struct fields or array elements inside a struct declaration:

    
Hangar_Display :: struct {
    luminance: float;

    ship: Spaceship;
    ship.model = "interceptor";  // Override just the model, keep other defaults.

    tint: Color;
    tint.b = 1.5;               // Boost the blue channel.

    luminance = 101.2;
}
    

Nested Structs

Structs can contain other structs as fields:

    
Gradient :: struct {
    start: Color;
    end:   Color;
    name:  string;
}
    

Assignment and Copying

Assigning one struct to another copies the data directly, byte for byte. There are no constructors, no deep copies, and no hidden behavior. If the struct contains pointers or strings, only the pointer values are copied, not the data they point to.

    
c := b;  // A simple memory copy of b into c.
print("c is: %\n", c);
    

Size and Layout

The size of a struct, measured in bytes, includes storage for all its fields plus any padding required for alignment. You can query the size with size_of:

    
print("size_of(Bounding_Box) = %\n", size_of(Bounding_Box));

// size_of takes a Type, not a value. If you have a value, use type_of:
print("size_of(type_of(b)) = %\n", size_of(type_of(b)));
    

The address-of operator * gives you the memory address of a value. For a struct, *b is the address where the struct begins, and *b.min_x is the address of the first field:

    
print("The base of b is %\n", *b);
print("The address of b.min_x is %\n", *b.min_x);  // Same as *b, since min_x is the first field.
print("The address of b.max_x is %\n", *b.max_x);
    

Struct Literals

You can write struct values directly in your code using struct literals. There are two styles: named and positional.

Named Struct Literals

You specify which fields to set by name. Any field you do not mention gets its default value:

    
loadout := Spaceship.{model = "interceptor", num_engines = 6};
print("loadout is: %\n", loadout);
    

When the type can be inferred from context, you can omit the type name on the left:

    
loadout2: Spaceship;
loadout2 = .{model = "cruiser", num_engines = 12};

// Works in procedure calls too:
run_diagnostics :: (ship: Spaceship) { /* ... */ }
run_diagnostics(.{num_engines = 8, has_shields = true});

// And in return statements:
parse_config :: (s: string) -> (bool, Spaceship) {
    if s == "heavy" return true, .{model = "dreadnought", num_engines = 24};
    return false, .{};  // .{} gives the default.
}
    

Named struct literals also support assigning sub-struct fields and array elements:

    
Payload :: struct {
    using header: Header;
    readings: [3] int;
    footer := "End.";
}

payload := Payload.{name = "Sensor_A", readings[1] = 7};
    

Positional Struct Literals

For structs with few fields, you can list values in order without names. You must provide a value for every settable field:

    
a := Color.{0.2, 0.5, 0.9};
b: Color = .{0, 0, 1};
    

Providing fewer values than there are fields is a compile-time error. This prevents a common class of bugs where a new field is added to a struct but existing literals are not updated. If you want to set only some fields, use the named style instead.

When the Type Must Be Explicit

In some situations, the compiler cannot infer the struct type, for example when passing a literal to a polymorphic procedure. In these cases you must write the type explicitly:

    
check :: (s: $T) { /* ... */ }

// check(.{num_engines = 8});                        // Error: type unknown.
check(Spaceship.{num_engines = 8});                   // Works.
    

Constant Struct Literals

If all the values in a struct literal are compile-time constants, the literal itself is considered constant. This can be verified with is_constant:

    
R22 :: 0.7071068;

result_1 := is_constant(Color.{R22, R22, 0});         // true: all values are constants.
result_2 := is_constant(Color.{1 - R22*R22, 0, 0});   // true: math between constants is constant.
    

Using with Structs

The using keyword brings a struct's fields into the current scope, allowing you to refer to them without the struct prefix. See using for full details.

    
{
    using hangar;

    luminance = 375.4;           // Same as hangar.luminance = 375.4
    ship.model = "cruiser";      // Same as hangar.ship.model = "cruiser"
    tint.r = 1;

    using tint;
    r += 1;  // Same as hangar.tint.r += 1
    g = -3;
    b *= 4;
}
    

If two using statements bring fields with the same name into scope, it is a compile-time error. See dealing with name collisions for strategies to resolve this.

Uninitialized Values

By default, all struct fields are initialized (either to explicit defaults or to zero). If you want to skip initialization for performance reasons, you can use ---:

    
c: Color = ---;  // c's fields contain whatever was in memory. No initialization.
    

Polymorphic Struct

A polymorphic struct is a struct parameterized by one or more compile-time constants, usually types or sizes. It is not itself a usable type but a blueprint: supplying concrete values for its parameters produces a distinct, fully formed struct type. Each unique combination of arguments yields a different type, and the parameters become accessible as members on both the type and its instances.

    
Buffer :: struct (T: Type, N: int) {
    items:  [N] T;
    cursor: int;
}
    

The parameters appear in parentheses after the struct keyword. They are always compile-time constants, so the leading $ that you see on polymorphic procedure parameters is not required here, although it is allowed. The parameters can be used inside the struct body as field types, array sizes, or anywhere else a constant of their kind is expected.

Instantiating a Polymorphic Struct

To create a usable type from a polymorphic struct, supply its arguments in parentheses:

    
Small_Buffer :: Buffer(float, 8);      // T becomes float, N becomes 8.
Large_Buffer :: Buffer(int, 4096);     // T becomes int,   N becomes 4096.

a: Small_Buffer;
b: Large_Buffer;
    

Each unique combination of parameters produces a different type. Buffer(float, 8) and Buffer(float, 16) are not interchangeable, even though they share the same blueprint.

Default Values for Parameters

Polymorphic struct parameters can have default values, just like procedure parameters:

    
Grid :: struct (Cell: Type, Width: int = 10, Height: int = 10) {
    cells: [Width][Height] Cell;
}

g: Grid(int);             // Width and Height default to 10.
h: Grid(float, 32, 32);   // All three explicitly specified.
    

Accessing Parameters as Members

A useful property of polymorphic structs is that their parameters become accessible on instances and on the type itself, using the same dot syntax you use for fields:

    
g: Grid(int, 5, 8);

print("Cell type is %\n", g.Cell);                    // => Cell type is s64
print("Dimensions are % by %\n", g.Width, g.Height);  // => 5 by 8
    

This works because the parameters are part of the concrete type. The compiler treats Grid(int, 5, 8) as a fully formed type whose name carries those values along with it.

Comparing Polymorphic Types

When comparing types, the comparison must include the parameters. The bare blueprint name is not itself a type:

    
g: Grid(int, 5, 8);

assert(type_of(g) == Grid(int, 5, 8));   // Works: full instantiation.
assert(g.Cell == int);                   // Works: parameter access.
// assert(type_of(g) == Grid);           // Error: Grid alone is not a type.
    

Partial Instantiation with #bake_arguments

You can fix some parameters of a polymorphic struct ahead of time and leave the rest open with #bake_arguments. This produces a new polymorphic blueprint with fewer remaining parameters:

    
Square_Grid :: #bake_arguments Grid(Width = 16, Height = 16);

inventory: Square_Grid(Cell = string);   // Only Cell still needs to be supplied.
    

A Recursive Example

Polymorphic structs are particularly useful when defining generic data structures. A doubly linked list can be written so that the element type is chosen at the point of use:

    
List :: struct (T: Type) {
    head: *Node(T);
    tail: *Node(T);
}

Node :: struct (T: Type) {
    value: T;
    prev:  *Node(T);
    next:  *Node(T);
}

// Each instantiation gives a distinct, fully typed list.
numbers: List(int);
words:   List(string);
    

Without polymorphism you would need to write a separate List and Node for every element type, or fall back to storing untyped pointers and casting. Polymorphic structs let the compiler generate the specialized versions for you.

Initialization and Literals

Once a polymorphic struct has been instantiated, it behaves like any other struct. You can declare values, assign defaults, and use struct literals exactly as before:

    
Pair :: struct (A: Type, B: Type) {
    first:  A;
    second: B;
}

p1 := Pair(int, string).{42, "hello"};
p2: Pair(float, float) = .{1.5, 2.5};
    

Note that when using a positional struct literal, you must spell out the full instantiated type, since the parameters are part of the type's identity.