# Chapter 6: Structs

**What you'll learn:** how to define a struct, construct it with named arguments, give it methods and operators, make modified copies with `with`, and what "value semantics" means when you assign a struct or pass it to a function.

Collections and tuples get you a long way, but a tuple `("db.local", 5432, true)` does not say which value is the port. A **struct** is a named record with named, typed fields. Structs are *value types*: assigning one copies it, and two structs with equal fields are equal. Classes (Chapter 8) are the reference types, for objects with identity, inheritance or shared state. This chapter covers structs (Language Reference §1-10).

## Defining and Constructing a Struct

```loup
@mode(host.scripted)

struct Endpoint {
    host: string;
    port: int;
    tls: bool = false;     # a default value
}

let db = Endpoint(host = "db.local", port = 5432);
let api = Endpoint(host = "api.example.com", port = 443, tls = true);

println(db);
println(api.host);
println(api.port + 1);
```

*Listing 6-1: A struct with a default field, constructed with named arguments*

```text
Endpoint(host: "db.local", port: 5432, tls: false)
api.example.com
444
```

A field is written `name: Type;`, with no keyword. The compiler generates the constructor: every field becomes a named parameter, and a field with a default (`tls: bool = false`) may be left out. Named arguments are written `name = value` (as here) or `name: value`.

### Mistake: positional arguments

If you come from Python's dataclasses or a C++ aggregate, you will try to pass the fields in order:

```loup,does_not_compile
#~ @mode(host.scripted)
struct Endpoint {
    host: string;
    port: int;
}

let db = Endpoint("db.local", 5432);
println(db);
```

```text
main.lpscr:7:10: error[E0001]: struct 'Endpoint' constructor requires named arguments: Endpoint(field1=value1, ...)
main.lpscr:7:10: error[E0001]: missing required field 'host' in 'Endpoint' constructor call
main.lpscr:7:10: error[E0001]: missing required field 'port' in 'Endpoint' constructor call
```

A struct constructor only takes named arguments, so that a call site cannot silently swap two fields of the same type. Name them:

```loup
#~ @mode(host.scripted)
struct Endpoint {
    host: string;
    port: int;
}

let db = Endpoint(host = "db.local", port = 5432);
println(db);
```

```text
Endpoint(host: "db.local", port: 5432)
```

A misspelled name is caught the same way — `Endpoint(host = "db.local", prot = 5432)` reports `unknown field 'prot' in struct 'Endpoint' constructor` and `missing required field 'port'`.

## Immutable and Mutable Fields

Fields are immutable unless declared with `var`:

```loup
#~ @mode(host.scripted)
struct Download {
    url: string;
    var bytesReceived: int = 0;
}

var d = Download(url = "https://example.com/data.csv");
d.bytesReceived = d.bytesReceived + 4096;
println(d);
```

```text
Download(url: "https://example.com/data.csv", bytesReceived: 4096)
```

### Mistake: writing an immutable field

```loup,does_not_compile
#~ @mode(host.scripted)
struct Download {
    url: string;
    var bytesReceived: int = 0;
}

var d = Download(url = "https://example.com/data.csv");
d.url = "https://mirror.example.com/data.csv";
```

```text
main.lpscr:8:1: error[E0001]: cannot assign to immutable field 'url' of struct 'Download'
```

`url` has no `var`, so it is fixed once the struct is built. Either declare it `var url: string;`, or — usually better — make a new value with a different URL using `with`, described below.

Write `var` on the *binding* too when you intend to change a struct's fields. (In Loup 1.25 a `let` binding does not stop writes to a `var` field, but the binding's keyword is how a reader learns that the value changes.)

## Value Semantics

Assigning a struct copies it. Every field is copied, including collections, so the two values are fully independent (§1-10.3.1):

```loup
@mode(host.scripted)

struct Settings {
    var retries: int;
    var hosts: list[string];
}

var original = Settings(retries = 3, hosts = ["alpha"]);
var tuned = original;
tuned.retries = 5;
tuned.hosts.push("beta");

println(original);
println(tuned);
```

*Listing 6-2: A struct copy is independent of the original*

```text
Settings(retries: 3, hosts: ["alpha"])
Settings(retries: 5, hosts: ["alpha", "beta"])
```

You never have to ask "who else holds this struct and might change it?" — nobody does.

### Mistake: changing a struct parameter

A parameter with no verb is a read-only borrow of the argument (§3-2.4.9), so a function cannot change the caller's struct by accident:

```loup,does_not_compile
@mode(host.scripted)

struct Settings {
    var retries: int;
}

func harden(s: Settings): void {
    s.retries = 10;
}

var settings = Settings(retries = 3);
harden(settings);
println(settings);
```

```text
main.lpscr:8:5: error[E_READS_MUTATION]: E_READS_MUTATION: parameter 's' is a `reads` borrow (a parameter without a verb is read-only, §3-2.4.9) and cannot be mutated (field 'retries' assigned); declare it `edits` and pass `edits: …` to change the caller's value, or copy it into a local first
```

There are two fixes. If the function exists to change the caller's value, declare the parameter `edits` and write `edits:` at the call, so the change is visible where it happens. If not, return a new value:

```loup
#~ @mode(host.scripted)
struct Settings {
    var retries: int;
}

# Fix 1: change the caller's value, visibly.
func harden(edits s: Settings): void {
    s.retries = 10;
}

# Fix 2: leave the argument alone and return a new value.
func hardened(s: Settings): Settings {
    return s with (retries = 10);
}

var a = Settings(retries = 3);
harden(edits: a);
println(a);

let b = hardened(Settings(retries = 1));
println(b);
```

```text
Settings(retries: 10)
Settings(retries: 10)
```

Chapter 19 explains the parameter verbs (`reads`, `edits`, `takes`, `shares`) in full.

## Equality

Two struct values are equal when all their fields are equal, compared field by field:

```loup
#~ @mode(host.scripted)
struct Point {
    x: float;
    y: float;
}

let a = Point(x = 1.5, y = 2.0);
let b = Point(x = 1.5, y = 2.0);
let c = Point(x = 3.0, y = 4.0);

println(a == b);
println(a == c);
```

```text
true
false
```

Classes, by contrast, compare by identity unless they define their own equality (Chapter 8).

## Modified Copies with `with`

`value with (field = newValue, …)` makes a copy of a struct with some fields replaced (§1-10.7). The original is untouched:

```loup
@mode(host.scripted)

struct Config {
    host: string;
    port: int;
    tls: bool;
    timeout: int;
}

let base = Config(host = "localhost", port = 8080, tls = false, timeout = 30);
let prod = base with (host = "prod.example.com", tls = true);

println(base);
println(prod);
```

*Listing 6-3: Deriving one configuration from another*

```text
Config(host: "localhost", port: 8080, tls: false, timeout: 30)
Config(host: "prod.example.com", port: 8080, tls: true, timeout: 30)
```

`with` works on immutable fields too, because it builds a new value rather than changing an existing one. It is the usual way to "change" a struct.

## Methods

A struct can declare methods. Inside a method, `this` is the value the method was called on:

```loup
@mode(host.scripted)

from core.math import (sqrt);

struct Point {
    x: float;
    y: float;

    ## Distance to another point.
    pub func distanceTo(other: Point): float {
        let dx = this.x - other.x;
        let dy = this.y - other.y;
        return sqrt(dx * dx + dy * dy);
    }

    ## A new point moved by (dx, dy).
    pub func translated(dx: float, dy: float): Point {
        return this with (x = this.x + dx, y = this.y + dy);
    }
}

let p = Point(x = 3.0, y = 4.0);
let origin = Point(x = 0.0, y = 0.0);
println(p.distanceTo(origin));
println(p.translated(1.5, -1.0));
```

*Listing 6-4: Methods that compute a value and return a modified copy*

```text
5
Point(x: 4.5, y: 3.0)
```

(A `float` with no fractional part prints as `5`; inside a struct or list it prints as `5.0`.)

Methods are `pub` when code in other modules should call them; fields without `pub` are visible only inside the declaring module (§1-10.2.1). In a single-file script that makes no difference.

### Mistake: a `mutating` method

Swift marks struct methods that change fields with `mutating`. Loup has no such keyword, and the parser reads `mutating` as the start of a field declaration:

```loup,does_not_compile
#~ @mode(host.scripted)
struct Counter {
    var count: int;

    pub mutating func increment(): void {
        this.count = this.count + 1;
    }
}
```

```text
main.lpscr:5:18: error[E0001]: expected `:` after field name in struct body
```

`modifier`, which marks mutating methods on classes (Chapter 8), is rejected on a struct with `E_MODIFIER_ON_STRUCT`. The idiomatic struct style is the one in Listing 6-4: methods return a new value, and the caller rebinds it:

```loup
#~ @mode(host.scripted)
struct Counter {
    count: int;

    pub func incremented(): Counter {
        return this with (count = this.count + 1);
    }
}

var c = Counter(count = 0);
c = c.incremented();
c = c.incremented();
println(c.count);
```

```text
2
```

> **Unsettled: assigning `this` fields in a method.** In Loup 1.25, a plain method may assign a `var` field through `this`, and the change is made to the value the method was called on. The Language Reference says the opposite (§1-10.5: inside a struct method `this` is not the caller's binding, so in-place mutation cannot reach it). Until the two agree, do not rely on it; return a new value or assign the field directly.

> **Known gap: static methods.** The Language Reference allows `static func` on structs (§1-10.5). In Loup 1.25 calling one, such as `Point.origin()`, compiles but fails at run time with `internal interpreter limitation`. Use a module-level function such as `func originPoint(): Point` instead.

## Operators

A struct can overload operators with `operator func`. Each operator belongs to an interface — `+` to `Add[Rhs, Out]`, `*` to `Mul[Rhs, Out]`, and so on — which the struct must name in its header:

```loup
@mode(host.scripted)

struct Vec2 implements Add[Vec2, Vec2] {
    x: float;
    y: float;

    pub operator func +(other: Vec2): Vec2 {
        return Vec2(x = this.x + other.x, y = this.y + other.y);
    }
}

let step = Vec2(x = 1.5, y = 2.0);
let drift = Vec2(x = 0.5, y = 1.0);
println(step + drift);
```

*Listing 6-5: Overloading `+` on a struct*

```text
Vec2(x: 2.0, y: 3.0)
```

### Mistake: an operator without its interface

```loup,does_not_compile
#~ @mode(host.scripted)
struct Vec2 {
    x: float;
    y: float;

    pub operator func +(other: Vec2): Vec2 {
        return Vec2(x = this.x + other.x, y = this.y + other.y);
    }
}

let v = Vec2(x = 1.5, y = 2.0);
println(v + v);
```

```text
main.lpscr:6:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '+' on 'Vec2' requires `implements Add` on the enclosing type
main.lpscr:12:9: error[E_OPERATOR_MISSING]: type 'Vec2' does not implement 'Add' required for operator '+' (E_OPERATOR_MISSING)
```

The first error is the cause; the second follows from it. Add `implements Add[Vec2, Vec2]` (right-hand operand type, result type) to the header, as in Listing 6-5. Chapter 9 covers interfaces.

## Choosing by Field Values

`match` (Chapter 7) works on struct values with guards:

```loup
#~ @mode(host.scripted)
struct Point {
    x: float;
    y: float;
}

func describe(p: Point): string {
    return match p {
        q if (q.x == 0.0 and q.y == 0.0) => "origin",
        q if (q.y == 0.0) => i"on the x axis at {$ q.x $}",
        q => i"({$ q.x $}, {$ q.y $})",
    };
}

println(describe(Point(x = 0.0, y = 0.0)));
println(describe(Point(x = 3.5, y = 0.0)));
println(describe(Point(x = 1.5, y = 2.5)));
```

```text
origin
on the x axis at 3.5
(1.5, 2.5)
```

> ⚠ **Planned.** The Language Reference (§1-10.8) defines struct patterns — `let Point(x = px, y = py) = p;` and arms such as `Point(x = 0.0, y = 0.0) => "origin"`. Loup 1.25 does not accept them; read the fields, or use a guard as above.

## Generic Structs

A struct can take type parameters in square brackets:

```loup
@mode(host.scripted)

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

func swap[A, B](p: Pair[A, B]): Pair[B, A] {
    return Pair(first = p.second, second = p.first);
}

let setting = Pair(first = "port", second = 8080);    # Pair[string, int]
println(setting.second + 1);
println(swap(setting));
```

*Listing 6-6: A generic struct and a generic function over it*

```text
8081
Pair(first: 8080, second: "port")
```

> **Known gap.** In Loup 1.25 a method of a generic struct cannot mention the struct's type parameter: `pub func get(): T` inside `struct Box[T]` reports `undefined type 'T'`. Write such operations as free generic functions, as `swap` is written here.

## What a Field May Hold

Because copies must be independent, a struct field cannot hold a class instance — copying the struct would share the instance (§1-10.3.1). Fields may be primitives, strings, collections, other structs, enums, tuples, functions, and `Option` or `Result` of those.

### Mistake: a class-typed field

```loup,does_not_compile
@mode(host.scripted)

class Connection {
    pub var host: string = "";
}

struct Request {
    path: string;
    conn: Connection;
}
```

```text
main.lpscr:7:1: error[E_STRUCT_CLASS_FIELD]: E_STRUCT_CLASS_FIELD: field 'conn' in struct 'Request' 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
```

The message lists the ways out. Most often, either the outer type should be a class (Chapter 8), or the struct should hold a value that identifies the connection — its host name, an ID — instead of the connection itself:

```loup
#~ @mode(host.scripted)
struct Request {
    path: string;
    host: string;
}

let r = Request(path = "/status", host = "api.example.com");
println(i"GET https://{$ r.host $}{$ r.path $}");
```

```text
GET https://api.example.com/status
```

## A Worked Example: An Invoice

Money is a classic value type. This version stores amounts in cents, so there is no floating-point rounding:

```loup
@mode(host.scripted)

## An amount of money in the smallest unit of its currency (cents).
struct Money {
    cents: int;
    currency: string;

    pub func plus(other: Money): Money {
        if (this.currency != other.currency) {
            throw ValueError(i"currency mismatch: {$ this.currency $} vs {$ other.currency $}");
        }
        return this with (cents = this.cents + other.cents);
    }

    pub func times(factor: int): Money {
        return this with (cents = this.cents * factor);
    }

    pub func percent(rate: int): Money {
        return this with (cents = this.cents * rate // 100);
    }

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

struct LineItem {
    description: string;
    price: Money;
    quantity: int;

    pub func subtotal(): Money {
        return this.price.times(this.quantity);
    }
}

let items = [
    LineItem(description = "Widget", price = Money(cents = 999, currency = "USD"), quantity = 3),
    LineItem(description = "Gadget", price = Money(cents = 1999, currency = "USD"), quantity = 1),
];

var total = Money(cents = 0, currency = "USD");
for (item in items) {
    println(i"{$ item.description $}: {$ item.subtotal().format() $}");
    total = total.plus(item.subtotal());
}

let tax = total.percent(8);
println(i"Subtotal: {$ total.format() $}");
println(i"Tax:      {$ tax.format() $}");
println(i"Total:    {$ total.plus(tax).format() $}");
```

*Listing 6-7: An invoice built from value types*

```text
Widget: 29.97 USD
Gadget: 19.99 USD
Subtotal: 49.96 USD
Tax:      3.99 USD
Total:    53.95 USD
```

Every value here is a plain struct: methods return new values instead of changing old ones, and `plus` throws `ValueError` (Chapter 10) when two currencies are mixed.

## Summary

- A struct is a named record: `struct Name { field: Type; var mutable: Type = default; }`.
- The generated constructor takes named arguments only (`Name(field = value)`); missing and unknown fields are compile errors.
- Fields are immutable unless declared `var`; `value with (field = x)` makes a modified copy.
- Structs are values: assignment copies them deeply, and `==` compares field by field.
- A parameter is a read-only borrow; use `edits` (and `edits:` at the call) or return a new value.
- Operators need the matching interface in the header: `implements Add[Vec2, Vec2]`.
- Struct fields cannot hold class instances (`E_STRUCT_CLASS_FIELD`).
- Not in Loup 1.25: struct patterns, static struct methods, and methods that mention a generic struct's type parameter.

## Try It Yourself

1. Define `struct Reading { tempC: float; humidity: float; }` and a method `toFahrenheit(): float` that returns `tempC * 9.0 / 5.0 + 32.0`.
2. Call `Money(cents = 100, currency = "USD").plus(Money(cents = 100, currency = "EUR"))` and read the uncaught `ValueError`. Then catch it after reading Chapter 10.
3. Add `implements Mul[int, Money]` and `pub operator func *(factor: int): Money` to `Money`, and replace `price.times(quantity)` with `price * quantity`.
4. Write `func scaled(edits p: Point, factor: float): void` for a `Point` with `var` fields and call it with `edits:`.
