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:
@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:
@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:
@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) = pandPoint(x = 0.0, y = 0.0) => …in amatcharm, 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):
@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
@reprand@alignfor freestanding code only, while the annotation table describes@repras "Freestanding/native". Stage0 1.25.1 accepts both annotations in ahost.nativefile, 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 unlessvar, optionally with defaults; the generated constructor takes named arguments only. - Structs are copied on assignment and compared field by field. Pass
editsto change a caller's struct. - Methods read fields through
this; operators areoperator funcplusimplements Add[…]and friends. Static methods on structs fail underloup runin 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.