The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

Chapter 7: Structs

A struct is a value type for plain data: copied on assignment, compared field by field, and never reference-counted. This chapter covers declaring and constructing structs, mutable fields, methods and operators, with copies, generic structs, and the rule that keeps structs pure values: no class-typed fields.

Declaring and constructing a struct

Struct fields are written name: Type; and are immutable unless declared var. A field may have a default value. The compiler generates a constructor that takes every field by name:

@mode(host.native)

struct RetryPolicy {
    attempts: int = 3;
    delayMs: int = 100;
    var backoff: float = 2.0;
}

func main(): void {
    let standard = RetryPolicy();
    let patient = RetryPolicy(attempts = 10, delayMs = 500);
    println(standard);
    println(patient);
    println(patient.attempts);
}

Listing 7-1: A struct with defaults and its generated constructor

RetryPolicy(attempts: 3, delayMs: 100, backoff: 2.0)
RetryPolicy(attempts: 10, delayMs: 500, backoff: 2.0)
10

Arguments must be named. A positional call, natural if you come from C or Rust tuple structs, is rejected, and the error lists every field it could not match:

This code does not compile
@mode(host.native)

struct Point {
    x: float;
    y: float;
}

func main(): void {
    let p = Point(1.0, 2.0);
    println(p);
}
main.lphc:9:13: error[E0001]: struct 'Point' constructor requires named arguments: Point(field1=value1, ...)
main.lphc:9:13: error[E0001]: missing required field 'x' in 'Point' constructor call
main.lphc:9:13: error[E0001]: missing required field 'y' in 'Point' constructor call

Write Point(x = 1.0, y = 2.0). A field without a default must always be given: Point(x = 1.0) is "missing required field 'y'".

A struct has no new(...) constructor of its own. If construction needs validation, write a function that checks its input and returns the struct (or a class, chapter 9).

Value semantics

Assigning a struct, passing it to a function, or storing it in a collection copies it. Two struct values are equal when all their fields are equal (Language Reference §1-10.3):

struct Counter {
    name: string;
    var count: int = 0;
}

func main(): void {
    var hits = Counter(name = "hits");
    var snapshot = hits;
    hits.count = hits.count + 1;
    println(hits);
    println(snapshot);
    println(snapshot == Counter(name = "hits"));
}

Listing 7-2: Assignment copies a struct

Counter(name: "hits", count: 1)
Counter(name: "hits", count: 0)
true

Changing hits did not change snapshot. Only var fields can be assigned:

This code does not compile
@mode(host.native)

struct Counter {
    name: string;
    var count: int = 0;
}

func main(): void {
    var hits = Counter(name = "hits");
    hits.name = "misses";
}
main.lphc:10:5: error[E0001]: cannot assign to immutable field 'name' of struct 'Counter'

To make a changed copy of an immutable field, use with (below).

Changing a caller's struct

Because a struct argument is copied, a function that assigns to its parameter's fields changes only its own copy. To change the caller's struct, take it as edits; then the parameter is the caller's value itself (Language Reference §3-2.4.9):

func record(edits c: Counter): void {
    c.count = c.count + 1;
}

func main(): void {
    var hits = Counter(name = "hits");
    record(edits: hits);
    record(edits: hits);
    println(hits.count);
}

Listing 7-3: An edits struct parameter

2

Methods and operators

Methods are declared inside the struct and read its fields through this. A method that "changes" a struct usually returns a new one. Operators are methods declared operator func. A first attempt at a money type usually looks like this:

This code does not compile
@mode(host.native)

struct Money {
    cents: int;

    pub operator func +(other: Money): Money {
        return Money(cents = this.cents + other.cents);
    }
}

func main(): void {
    let total = Money(cents = 1999) + Money(cents = 501);
    println(total.cents);
}
main.lphc:6:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '+' on 'Money' requires `implements Add` on the enclosing type
main.lphc:12:17: error[E_OPERATOR_MISSING]: type 'Money' does not implement 'Add' required for operator '+' (E_OPERATOR_MISSING)

Each operator belongs to an interface, and the type must declare it: + is Add[Rhs, Result], with the right-hand operand type and the result type as arguments (Language Reference §1-5.1):

struct Money implements Add[Money, Money] {
    cents: int;

    pub operator func +(other: Money): Money {
        return Money(cents = this.cents + other.cents);
    }

    pub func format(): string {
        let dollars = this.cents // 100;
        let rest = this.cents % 100;
        let pad = if (rest < 10) { "0" } else { "" };
        return i"${$ dollars $}.{$ pad $}{$ rest $}";
    }
}

func main(): void {
    var total = Money(cents = 0);
    for (price in [Money(cents = 1999), Money(cents = 501)]) {
        total = total + price;
    }
    println(total.format());
}

Listing 7-4: A money type with +

$25.00

Money is kept in integer cents because decimal is not available yet (chapter 3).

The specification also allows pub static func on structs (for example a Money.zero() factory). In Stage0 1.25.1 such a call compiles but stops loup run with internal interpreter limitation: unsupported instruction: CallVirtual zero on None, so use a module-level function or a constructor call with defaults instead.

with copies

value with (field = newValue, …) makes a copy with some fields replaced. It works on immutable fields, which is its main use:

let base = RetryPolicy();
let aggressive = base with (attempts = 8, delayMs = 20);
println(base);
println(aggressive);

Listing 7-5: Copy with changes

RetryPolicy(attempts: 3, delayMs: 100, backoff: 2.0)
RetryPolicy(attempts: 8, delayMs: 20, backoff: 2.0)

A misspelled field is caught: base with (atempts = 8) is E_WITH_COPY_UNKNOWN_FIELD: field 'atempts' does not exist on struct 'RetryPolicy'. with on a class instance is E_WITH_COPY_WRONG_TYPE: only values can be copied this way.

⚠ Planned. Struct destructuring, let Point(x = px, y = py) = p and Point(x = 0.0, y = 0.0) => … in a match arm, is specified (Language Reference §1-10.8) but does not parse in 1.25.1. Read the fields instead, and match on their values with guards.

Generic structs

A struct can take type parameters. They are usually inferred from the constructor arguments:

struct Pair[A, B] {
    first: A;
    second: B;
}

func main(): void {
    let labelled = Pair(first = "port", second = 8080);
    let explicit = Pair[int, bool](first = 1, second = true);
    println(labelled);
    println(explicit.second);
}

Listing 7-6: A generic struct

Pair(first: "port", second: 8080)
true

Chapter 11 covers generics in general.

Structs hold values, not objects

A struct may contain primitives, strings, bytes, collections, enums, tuples and other structs. It may not contain a class instance (Language Reference §1-10.3.1, the pure value type invariant):

This code does not compile
@mode(host.native)

class Connection {
    pub let host: string;
    new(host: string) { this.host = host; }
}

struct Session {
    id: int;
    conn: Connection;
}

func main(): void {
    let s = Session(id = 1, conn = Connection("db"));
}
main.lphc:8:1: error[E_STRUCT_CLASS_FIELD]: E_STRUCT_CLASS_FIELD: field 'conn' in struct 'Session' has class type 'Connection'. Migration: (a) convert struct to class, (b) replace field with a value type, (c) use structures.* with a class, or (d) add T: ValueType constraint

If a struct could hold a class reference, copying the struct would silently share the object, and a "copy" could be changed through someone else's reference. The rule keeps "struct" meaning "independent value". The message lists the ways out; the usual one is (a): when something holds a connection, it is an object with identity, so make it a class:

class Connection {
    pub let host: string;
    new(host: string) { this.host = host; }
}

class Session {
    pub let id: int;
    pub let conn: Connection;
    new(id: int, conn: Connection) {
        this.id = id;
        this.conn = conn;
    }
}

func main(): void {
    let s = Session(1, Connection("db.local"));
    println(i"session {$ s.id $} on {$ s.conn.host $}");
}

Listing 7-7: A class may hold another class

session 1 on db.local

host.native adds one exception: a struct may hold a unique value, which makes the struct itself move-only. Chapter 17 covers it.

Layout for C

C libraries expect structs laid out by the C rules: fields in declaration order with natural alignment. The @repr(C) and @align(n) annotations request that layout (Language Reference §3-3.10, §3-3.11).

⚠ Planned. The feature matrix (Language Reference §1-2.6) lists @repr and @align for freestanding code only, while the annotation table describes @repr as "Freestanding/native". Stage0 1.25.1 accepts both annotations in a host.native file, but there is no way to hand such a struct to C code from a native program yet: the FFI library (native.ffi) is not shipped. Chapter 19 covers what native FFI exists today; The Loup Freestanding Book covers struct layout in depth.

Summary

  • Struct fields are name: Type;, immutable unless var, optionally with defaults; the generated constructor takes named arguments only.
  • Structs are copied on assignment and compared field by field. Pass edits to change a caller's struct.
  • Methods read fields through this; operators are operator func plus implements Add[…] and friends. Static methods on structs fail under loup run in 1.25.1.
  • value with (field = v) makes a modified copy. Struct destructuring is ⚠ planned.
  • A struct may not contain a class instance (E_STRUCT_CLASS_FIELD); make the container a class instead.
  • @repr(C) and @align(n) are accepted but not usable for native interop yet.
  • Chapter 8 covers enums and pattern matching.