Linear Types

Gruel's type system has three levels of ownership discipline:

AnnotationBehaviour
(none)Affine — used at most once; can be silently dropped
linearLinear — must be consumed exactly once
@derive(Copy)Copy — implicitly duplicated on use

You've already seen affine structs (move semantics) and @derive(Copy) structs. This page covers linear types and the Handle interface for explicit duplication.

Linear Types Must Be Consumed

Mark a struct with linear to require that every value of that type is explicitly consumed — it is a compile error to let one go out of scope without being used:

linear struct Token { value: i32 }

fn use_token(t: Token) -> i32 { t.value }

fn main() -> i32 {
    let t = Token { value: 42 };
    use_token(t)  // t is consumed here — OK
}

If you create a linear value and never consume it, the compiler rejects the program:

linear struct Token { value: i32 }

fn main() -> i32 {
    let t = Token { value: 1 };
    // ERROR: linear value `t` dropped without being consumed
    0
}

This is useful for types that represent resources requiring explicit action — tokens that must be redeemed, handles that must be closed, results that must be checked.

Consuming a Linear Value

A linear value is consumed by any of these:

  • Passing it to a function (by value)
  • Returning it from a function
  • Accessing a field (the linear value is consumed by the access)
  • Destructuring it with a let binding (all fields are transferred)
linear struct Ticket { id: i32 }

fn redeem(t: Ticket) -> i32 { t.id }

fn main() -> i32 {
    let t = Ticket { id: 7 };
    redeem(t)
}

Chaining Through Functions

You can transform a linear value by passing it to a function that returns a new one. The chain preserves the must-consume guarantee:

linear struct Value { n: i32 }

fn double(v: Value) -> Value {
    Value { n: v.n * 2 }
}

fn finish(v: Value) -> i32 { v.n }

fn main() -> i32 {
    let v = Value { n: 21 };
    let v2 = double(v);
    finish(v2)   // prints 42
}

All Branches Must Consume

If a linear value might go unconsumed in any branch, the compiler errors. You must consume it in every branch:

linear struct Permit { id: i32 }

fn use_it(p: Permit) -> i32 { p.id }

fn main() -> i32 {
    let p = Permit { id: 42 };
    let cond = true;
    if cond {
        use_it(p)   // consumed in true branch
    } else {
        use_it(p)   // consumed in false branch — both required
    }
}

Linear Types Cannot Be @derive(Copy)

Allowing implicit copies would defeat the purpose of linear types — you could copy before dropping to avoid the consume requirement. The compiler rejects this combination:

@derive(Copy)
linear struct Bad { value: i32 }
// ERROR: linear struct cannot be marked `@derive(Copy)`

Explicit Duplication with Handle

Sometimes you legitimately need two handles to the same logical resource — for example, forking a value for two code paths. The compiler-recognized Handle interface enables this with explicit syntax. A type conforms to Handle by defining a method with the exact shape fn handle(self: Ref(Self)) -> Self:

struct Counter {
    count: i32,

    fn handle(self: Ref(Self)) -> Counter {
        Counter { count: self.count }
    }
}

fn main() -> i32 {
    let a = Counter { count: 42 };
    let b = a.handle();  // explicit duplication — cost is visible
    a.count + b.count    // a is still valid; .handle() reads through Ref
}

There is no @derive(Handle) directive — conformance is structural. If a type defines that exact method, it conforms; otherwise it doesn't. Unlike @derive(Copy), duplication is never implicit. You must call .handle(), making the cost visible at every use site.

Handle with linear

Handle and linear compose freely. This gives you explicit duplication while still requiring every handle to be consumed:

linear struct Task {
    id: i32,

    fn handle(self: Ref(Self)) -> Task {
        Task { id: self.id }
    }
}

fn run(t: Task) -> i32 { t.id }

fn main() -> i32 {
    let t = Task { id: 42 };
    let t2 = t.handle();  // fork — both handles must be consumed
    run(t) + run(t2)      // consume both
}

This is the one place Handle and Clone diverge: Clone is rejected on linear types because cloning would create an unaccounted-for second linear value, but Handle is allowed because .handle() is an explicit, opt-in fork.

When to Use Linear Types

Use linear when:

  • A value represents a resource that must be explicitly finalized (a connection that must be committed or rolled back, a lock that must be released)
  • You want the type system to guarantee a result is checked before it is discarded
  • You're encoding a protocol where skipping a step should be a compile error

For most types, affine semantics (the default) are sufficient — values can be dropped without any action.