# Chapter 7: Structs

**What you'll learn.** How to declare and construct structs, what value semantics means in practice, the `with` copy expression, methods and operators on structs, the pure-value invariant, and generic structs.

**Why it matters.** Structs are the type you reach for when modelling data: a point, a viewport size, a line item on an invoice, a form submission. Because a struct is copied on assignment, a function that receives one cannot change the caller's copy behind its back. That removes the "who else is holding this object?" question that JavaScript objects and Python dicts leave open.

A class is an object with identity: two names can refer to the same instance, and a change made through one is visible through the other. A struct is a value: like an `int`, it is copied whenever it is assigned or passed. This chapter is about the second kind. Classes come in Chapter 9.

## Declaring and constructing a struct

A struct declares its fields with a name and a type. Fields take no `let` keyword; they are immutable unless marked `var`. A field may have a default value.

```loup
@mode(web.compiled)

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

struct Viewport {
    width: int = 800;
    height: int = 600;
    scale: float = 1.0;
}

func main(): void {
    let origin = Point(x = 0.0, y = 0.0);
    let p = Point(x = 3.0, y = 4.0);
    println(origin);
    println(p.x + p.y);

    let defaults = Viewport();
    let hd = Viewport(width = 1920, height = 1080);
    println(defaults);
    println(hd);
}
```

*Listing 7-1: Declaring structs and constructing them with named arguments*

```text
Point(x: 0.0, y: 0.0)
7
Viewport(width: 800, height: 600, scale: 1.0)
Viewport(width: 1920, height: 1080, scale: 1.0)
```

The compiler generates a constructor for every struct. Its parameters are the fields, by name, and a field with a default may be left out. `println` renders a struct structurally, as `Name(field: value, …)`, so you rarely need to write a formatting function just to look at one. (A whole `float` printed on its own, like `p.x + p.y` above, prints as `7`; inside a rendered struct it keeps its `.0`.)

In TypeScript you would write `{ x: 0, y: 0 }` and hope every caller spells the keys the same way. In Loup the struct name is the constructor and the compiler checks every argument.

### Mistake: positional arguments

Coming from Python or C, you might try to construct a struct positionally:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Point {
    x: float;
    y: float;
}

func main(): void {
    let p = Point(1.0, 2.0);
    println(p);
}
```

```text
main.lpweb:8:13: error[E0001]: struct 'Point' constructor requires named arguments: Point(field1=value1, ...)
main.lpweb:8:13: error[E0001]: missing required field 'x' in 'Point' constructor call
main.lpweb:8:13: error[E0001]: missing required field 'y' in 'Point' constructor call
```

The generated constructor has only named parameters, so the two positional values do not fill any field; the compiler then also reports both fields as missing. Name each field: `Point(x = 1.0, y = 2.0)`. (Named arguments may also be written with a colon, `Point(x: 1.0, y: 2.0)`.)

### Mistake: an `int` where a `float` is expected

```loup,does_not_compile
#~ @mode(web.compiled)
struct Point {
    x: float;
    y: float;
}

func main(): void {
    let p = Point(x = 1, y = 2);
    println(p);
}
```

```text
main.lpweb:8:19: error[E0001]: field 'x' of 'Point': expected 'float', found 'int'
main.lpweb:8:26: error[E0001]: field 'y' of 'Point': expected 'float', found 'int'
```

Loup never converts between `int` and `float` for you (Chapter 3). Write the literals as floats:

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

func main(): void {
    let p = Point(x = 1.0, y = 2.0);
    println(p);
}
```

```text
Point(x: 1.0, y: 2.0)
```

## Immutable and `var` fields

A field without `var` cannot be assigned after construction:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Point {
    x: float;
    y: float;
}

func main(): void {
    var p = Point(x = 1.0, y = 2.0);
    p.x = 5.0;
    println(p);
}
```

```text
main.lpweb:9:5: error[E0001]: cannot assign to immutable field 'x' of struct 'Point'
```

Making the binding `var` does not help: `var p` lets you replace the whole value (`p = Point(…)`), but the fields of `Point` are still immutable. If a field really changes over the struct's life, declare it `var`:

```loup
#~ @mode(web.compiled)
struct ClickCounter {
    label: string;
    var count: int = 0;
}

func main(): void {
    var clicks = ClickCounter(label = "signup button");
    clicks.count = clicks.count + 1;
    clicks.count = clicks.count + 1;
    println(clicks);
}
```

*Listing 7-2: A struct with one mutable field*

```text
ClickCounter(label: "signup button", count: 2)
```

Bind a struct with `var` when you intend to change its fields. The 1.25.1 compiler does not yet reject a write to a `var` field through a `let` binding, so this is a convention you keep yourself for now.

Often you don't need a `var` field at all: you can make a new value with the change applied, using `with` (below).

## Value semantics

Assigning a struct copies it. After the copy, the two values are independent:

```loup
@mode(web.compiled)

struct Pixel {
    var x: int;
    var y: int;
}

struct Cart {
    owner: string;
    var items: list[string];
}

func main(): void {
    var a = Pixel(x = 1, y = 1);
    var b = a;
    b.x = 99;
    println(a);
    println(b);

    let mine = Cart(owner = "ann", items = ["apple"]);
    var theirs = mine;
    theirs.items.push("pear");
    println(mine.items);
    println(theirs.items);

    println(a == Pixel(x = 1, y = 1));
}
```

*Listing 7-3: Assignment copies a struct, including the collections inside it*

```text
Pixel(x: 1, y: 1)
Pixel(x: 99, y: 1)
["apple"]
["apple", "pear"]
true
```

Three things to notice:

- Changing `b.x` does not change `a`.
- The `list` inside `Cart` is copied too. The built-in collections are values in Loup (Language Reference §1-10.3, ruling COLL-1), so pushing onto `theirs.items` leaves `mine.items` alone. In JavaScript, `const theirs = { ...mine }` would have shared the array.
- `==` compares structs field by field.

### Known gap: a struct read out of a list is not yet a copy

The Language Reference says that reading a struct out of a list or dict element gives you an independent copy (§1-10.3). The 1.25.1 interpreter does not do this yet: the binding refers to the element itself, and even a `with` copy of the element writes into the list.

```loup,not_desired_behavior
#~ @mode(web.compiled)
struct Pixel {
    var x: int;
    var y: int;
}

func main(): void {
    var points = [Pixel(x = 1, y = 1), Pixel(x = 2, y = 2)];
    var first = points[0];
    first.x = 99;
    println(points[0]);
    let shifted = points[1] with (y = 20);
    println(points[1]);
}
```

```text
Pixel(x: 99, y: 1)
Pixel(x: 2, y: 20)
```

Both changes leak into `points`. Until this is fixed, don't change a struct you read out of a collection. Build the new value with the constructor and store it back:

```loup
#~ @mode(web.compiled)
struct Pixel {
    var x: int;
    var y: int;
}

func main(): void {
    var points = [Pixel(x = 1, y = 1), Pixel(x = 2, y = 2)];
    let old = points[0];
    points[0] = Pixel(x = 99, y = old.y);
    println(old);
    println(points);
}
```

```text
Pixel(x: 1, y: 1)
[Pixel(x: 99, y: 1), Pixel(x: 2, y: 2)]
```

### Known gap: structs as dict keys and set members

Structs are specified to be hashable when their fields are (§1-10.3), so a struct should work as a dict key. In 1.25.1 it does not: the lookup never finds the key, not even with the same binding.

```loup,not_desired_behavior
#~ @mode(web.compiled)
struct Cell {
    row: int;
    col: int;
}

func main(): void {
    let home = Cell(row = 1, col = 1);
    let labels = {home: "home"};
    println(labels.contains(home));
}
```

```text
false
```

Use a tuple of the fields as the key instead; tuples hash by value today:

```loup
#~ @mode(web.compiled)
struct Cell {
    row: int;
    col: int;
}

func main(): void {
    let home = Cell(row = 1, col = 1);
    let labels = {(home.row, home.col): "home"};
    println(labels.contains((1, 1)));
    println(labels[(home.row, home.col)]);
}
```

```text
true
home
```

## Structs as function parameters

A parameter with no verb is a read-only borrow (`reads`, Language Reference §3-2.4.9). The function can look at the struct but not change it:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Position {
    var x: float;
    var y: float;
}

func nudge(p: Position, dx: float): void {
    p.x = p.x + dx;
}

func main(): void {
    var pos = Position(x = 1.0, y = 1.0);
    nudge(pos, 2.5);
    println(pos);
}
```

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

The message names both fixes. If the function should change the caller's value, declare the parameter `edits`:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Position {
    var x: float;
    var y: float;
}

func nudge(edits p: Position, dx: float): void {
    p.x = p.x + dx;
}

func main(): void {
    var pos = Position(x = 1.0, y = 1.0);
    nudge(pos, 2.5);
    println(pos);
}
```

```text
main.lpweb:13:11: error[E_VERB_REQUIRED]: E_VERB_REQUIRED: parameter 'p' requires 'edits' verb acknowledgment at call site
```

An `edits` parameter has to be acknowledged where it is called, so that a reader of `main` can see that `pos` may change. Write `edits:` before the argument:

```loup
#~ @mode(web.compiled)
struct Position {
    var x: float;
    var y: float;
}

func nudge(edits p: Position, dx: float): void {
    p.x = p.x + dx;
}

func main(): void {
    var pos = Position(x = 1.0, y = 1.0);
    nudge(edits: pos, 2.5);
    nudge(edits: pos, 2.5);
    println(pos);
}
```

*Listing 7-4: An `edits` parameter changes the caller's struct, and the call site says so*

```text
Position(x: 6.0, y: 1.0)
```

If the function only needs a changed value for its own use, the other fix from the message applies: copy the parameter into a local (`var q = p;`) and change that. Chapter 9 covers the other ownership verbs, `takes` and `shares`.

## The `with` copy

`value with (field = newValue, …)` makes a new struct that is a copy of `value` except for the listed fields:

```loup
#~ @mode(web.compiled)
struct Viewport {
    width: int;
    height: int;
    scale: float = 1.0;
}

func main(): void {
    let hd = Viewport(width = 1920, height = 1080);
    let retina = hd with (scale = 2.0);
    let small = hd with (width = 1280, height = 720);
    println(hd);
    println(retina);
    println(small);
}
```

*Listing 7-5: `with` copies a struct and changes some fields*

```text
Viewport(width: 1920, height: 1080, scale: 1.0)
Viewport(width: 1920, height: 1080, scale: 2.0)
Viewport(width: 1280, height: 720, scale: 1.0)
```

`hd` is unchanged. `with` works on immutable fields, which is the usual way to "change" a struct whose fields are not `var`. Naming a field the struct does not have is `E_WITH_COPY_UNKNOWN_FIELD`, and `with` on a class instance is `E_WITH_COPY_WRONG_TYPE`: only structs are copied this way.

## Methods

A struct can declare methods. Inside a method, the struct's fields are reached through `this`:

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

    pub func length(): float {
        return (x * x + y * y) ** 0.5;
    }
}

func main(): void {
    println(Vec2(x = 3.0, y = 4.0).length());
}
```

```text
main.lpweb:7:17: error[E0001]: undefined identifier `x`
main.lpweb:7:21: error[E0001]: undefined identifier `x`
main.lpweb:7:25: error[E0001]: undefined identifier `y`
main.lpweb:7:29: error[E0001]: undefined identifier `y`
```

Unlike Java or C#, a bare field name is not in scope inside a method. Write `this.x`:

```loup
#~ @mode(web.compiled)
struct Vec2 {
    x: float;
    y: float;

    pub func length(): float {
        return (this.x * this.x + this.y * this.y) ** 0.5;
    }

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

    pub func scaled(k: float): Vec2 {
        return Vec2(x = this.x * k, y = this.y * k);
    }
}

func main(): void {
    let v = Vec2(x = 3.0, y = 4.0);
    println(v.length());
    println(v.plus(Vec2(x = 1.0, y = 1.0)));
    println(v.scaled(0.5));
}
```

*Listing 7-6: Struct methods read fields through `this` and return new values*

```text
5
Vec2(x: 4.0, y: 5.0)
Vec2(x: 1.5, y: 2.0)
```

`plus` and `scaled` return new values instead of changing `this`. That is the natural style for a value type. (`** 0.5` takes the square root. A web-compiled file cannot import `core.math` for `sqrt`; see Chapter 2, [ch02-compiled-mode-rules.md](ch02-compiled-mode-rules.md), section "The standard library from web code".)

### Mistake: `modifier` on a struct method

If you have read ahead to classes, you might mark a mutating struct method `modifier`:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Counter {
    var n: int;

    pub modifier func increment(): void {
        this.n = this.n + 1;
    }
}

func main(): void {
    var c = Counter(n = 0);
    c.increment();
    println(c.n);
}
```

```text
main.lpweb:5:5: error[E_MODIFIER_ON_STRUCT]: E_MODIFIER_ON_STRUCT: `modifier` keyword on method 'increment' of struct 'Counter' is not allowed — `modifier` applies only to class methods
main.lpweb:5:5: error[E_MODIFIER_ON_STRUCT]: E_MODIFIER_ON_STRUCT: `modifier` keyword on method 'increment' of struct 'Counter' is not allowed — `modifier` applies only to class methods
```

`modifier` is the mutation contract for class instances, which are shared by reference (Chapter 9). For a struct, return the updated value, or write a function with an `edits` parameter as in Listing 7-4:

```loup
#~ @mode(web.compiled)
struct Counter {
    var n: int;

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

func increment(edits c: Counter): void {
    c.n = c.n + 1;
}

func main(): void {
    var c = Counter(n = 0);
    c = c.incremented();
    increment(edits: c);
    println(c.n);
}
```

```text
2
```

### Static methods

The Language Reference allows `pub static func` on a struct (§1-10.5), for factories such as `Vec2.zero()`. The 1.25.1 compiler accepts the declaration, but calling one fails at run time with `internal interpreter limitation: unsupported instruction: CallVirtual zero on None`. Until that is fixed, write the factory as a module-level function, for example `func zeroVec(): Vec2`.

## Operators on structs

A struct can overload an operator with `operator func`. The struct must also say which operator interface it implements:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Money {
    cents: int;

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

func main(): void {
    println(Money(cents = 1999) + Money(cents = 501));
}
```

```text
main.lpweb:5:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '+' on 'Money' requires `implements Add` on the enclosing type
main.lpweb:11:13: error[E_OPERATOR_MISSING]: type 'Money' does not implement 'Add' required for operator '+' (E_OPERATOR_MISSING)
```

`Add` takes two type arguments: the right-hand operand type and the result type. Declare it on the struct:

```loup
#~ @mode(web.compiled)
struct Money implements Add[Money, Money] {
    cents: int;
    currency: string;

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

func main(): void {
    let price = Money(cents = 1999, currency = "USD");
    let shipping = Money(cents = 501, currency = "USD");
    println(price + shipping);
}
```

*Listing 7-7: `+` on a struct, declared through `implements Add[Money, Money]`*

```text
Money(cents: 2500, currency: "USD")
```

Money is held in integer cents here, which avoids binary floating-point rounding; Chapter 3 covers the numeric types. Structs can implement any interface the same way; Chapter 10 covers interfaces.

## The pure-value invariant

A struct field cannot have a class type (Language Reference §1-10.3.1):

```loup,does_not_compile
#~ @mode(web.compiled)
class Customer {
    pub let name: string;
    new(name: string) { this.name = name; }
}

struct Invoice {
    customer: Customer;
    totalCents: int;
}

func main(): void {
    println("never runs");
}
```

```text
main.lpweb:7:1: error[E_STRUCT_CLASS_FIELD]: E_STRUCT_CLASS_FIELD: field 'customer' in struct 'Invoice' has class type 'Customer'. 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 instance, copying the struct would copy only the reference, and two "independent" copies would share one `Customer`. The rule keeps every struct copy fully independent. The diagnostic lists the ways out; the most common is (b), keeping the data you need instead of the object:

```loup
#~ @mode(web.compiled)
struct Invoice {
    customerName: string;
    totalCents: int;
}

func main(): void {
    println(Invoice(customerName = "Ann Lee", totalCents = 4250));
}
```

```text
Invoice(customerName: "Ann Lee", totalCents: 4250)
```

A struct field may be a primitive, another struct, an enum, a tuple, a function type, `Option`/`Result`, or a built-in collection (`list`, `dict`, `set`) whose elements follow the same rule. If the data needs identity, such as an object several parts of the program update, make it a class instead.

For a generic struct, the rule is checked against each type argument. A type parameter can declare the `ValueType` bound (`struct Box[T: ValueType]`) to say so up front; the 1.25.1 compiler accepts the bound but does not yet reject a class as the type argument.

## Destructuring

> ⚠ **Planned.** The Language Reference specifies struct patterns, `let Point(x = px, y = py) = p;` and `Point(x = 0.0, y = 0.0) => …` in a `match` (§1-10.8). The 1.25.1 compiler does not bind the names in either form, so read the fields (`p.x`) instead. Tuple destructuring, `let (w, h) = (1920, 1080);`, works today (Chapter 6).

## Generic structs

A struct can take type parameters in square brackets:

```loup
#~ @mode(web.compiled)
struct Pair[A, B] {
    first: A;
    second: B;
}

struct Range[T] {
    start: T;
    end: T;
}

func main(): void {
    let entry = Pair(first = "retries", second = 3);
    let window: Range[int] = Range(start = 0, end = 100);
    println(entry);
    println(entry.second + 1);
    println(window.end - window.start);
}
```

*Listing 7-8: Generic structs; the type arguments are inferred from the constructor*

```text
Pair(first: "retries", second: 3)
4
100
```

Chapter 11 covers generics, including bounds.

## A complete example: an invoice

Here is a small invoice model. Every function takes values and returns new ones; nothing is changed in place.

```loup
@mode(web.compiled)

struct LineItem {
    sku: string;
    unitCents: int;
    quantity: int;
}

struct Invoice {
    number: string;
    lines: list[LineItem];
    taxPercent: int = 0;
}

func lineTotal(item: LineItem): int {
    return item.unitCents * item.quantity;
}

func withLine(invoice: Invoice, item: LineItem): Invoice {
    var lines = invoice.lines;
    lines.push(item);
    return invoice with (lines = lines);
}

func subtotal(invoice: Invoice): int {
    var sum = 0;
    for (item in invoice.lines) {
        sum = sum + lineTotal(item);
    }
    return sum;
}

func total(invoice: Invoice): int {
    let net = subtotal(invoice);
    return net + net * invoice.taxPercent // 100;
}

func dollars(cents: int): string {
    let rest = cents % 100;
    let padded = if (rest < 10) { "0" + str(rest) } else { str(rest) };
    return "$" + str(cents // 100) + "." + padded;
}

func main(): void {
    let empty = Invoice(number = "INV-1042", lines = [], taxPercent = 8);
    let one = withLine(empty, LineItem(sku = "MUG-01", unitCents = 1250, quantity = 2));
    let two = withLine(one, LineItem(sku = "TEA-07", unitCents = 899, quantity = 1));

    println(empty.lines.length());
    println(two.lines.length());
    println("subtotal " + dollars(subtotal(two)));
    println("total    " + dollars(total(two)));
}
```

*Listing 7-9: An invoice built from struct values; `withLine` returns a new invoice*

```text
0
2
subtotal $33.99
total    $36.70
```

`withLine` copies the invoice's list into a local, pushes onto the copy, and returns `invoice with (lines = lines)`. Because lists are values, `empty` still has no lines after `one` and `two` are built from it. This is the same discipline that Redux reducers or Immer ask for in JavaScript, except that here the copies are what the language does by default.

## Try it yourself

1. Give `Invoice` a `discountCents: int = 0` field and change `total` to subtract it before tax. Use `with` to apply a discount to `two` and print both totals.
2. Write `func moved(p: Position, dx: float, dy: float): Position` that returns a new position, and compare it with the `edits` version in Listing 7-4. Which one would you rather call from three places?
3. Add a `customer: Customer` class field to `Invoice` and read the diagnostic. Then fix it by storing a customer ID instead.

## Summary

- A struct is a value type: assigning or passing it copies it, including any `list`, `dict` or `set` fields, and `==` compares field by field.
- Construct structs with named arguments; fields with defaults may be omitted. There are no positional constructors and no implicit `int`→`float` conversion.
- Fields are immutable unless declared `var`. `value with (field = v)` makes a modified copy.
- A parameter without a verb is read-only (`E_READS_MUTATION`); an `edits` parameter changes the caller's struct and must be acknowledged at the call site with `edits:`.
- Methods read fields through `this` and usually return new values. `modifier` is for classes only (`E_MODIFIER_ON_STRUCT`).
- Operators need both `operator func` and the matching interface, such as `implements Add[Money, Money]`.
- A struct field cannot be a class (`E_STRUCT_CLASS_FIELD`).
- Known gaps in 1.25.1: a struct read out of a list element is not yet copied, structs do not work as dict keys or set members, struct static methods fail when called, and struct destructuring patterns do not bind.
