Skip to content

Ownership and borrowing

Move-only values, default borrows, sink transfers, inout mutation, drop, and loop restrictions in Prismio 0.1.

Last verified

Ownership determines which binding is responsible for move-only data and when an operation may read, mutate, transfer, or destroy it. Prismio 0.1 applies these checks without source-level reference syntax.

Strings, lists, and structs are move-only. Optional wrappers around owned reference-shaped values preserve that ownership. Scalars, raw pointer values, fieldless enums, and arrays of a known length are copied as values in the current model. A [T] parameter is the exception among arrays: it is a view of the caller's array, so a store through it writes the caller's elements — see copies and views.

OperationCopy valueMove-only value
Assign to a new owned bindingcopymove
Pass to an ordinary parametercopyborrow
Pass to sinkcopy-like call valuemove
Pass to inoutmutable accessexclusive mutable borrow
drop(value)rejectedconsumes owned value

The compiler tracks a move-only binding and rejects later reads after ownership transfers.

Moves

Assignment to a new owned location moves a string, list, or struct:

prismio
import std.io

struct Record { id: Int }

fn main() -> Int {
    let first = Record { id: 3 }
    let second = first
    println(second.id)
    return first.id
}

The initializer for second transfers the record. The original first binding remains in lexical scope, but its state is moved and it cannot be read.

Moves also occur when a value is stored into an owning struct field, inserted into an owning list, passed to a sink parameter, or explicitly dropped. mut controls reassignment; it does not make a moved value readable again.

Borrow by default

Passing a move-only value to an ordinary parameter borrows it. The caller can use the value after the call.

prismio
struct Box { value: Int }
fn read(box: Box) -> Int { return box.value }

fn main() -> Int {
    let box = Box { value: 7 }
    let first = read(box)
    let second = read(box)
    return second - first
}

The signature contains no borrow keyword for ordinary Prismio functions. Borrowing is the default parameter behavior for move-only values, and the caller writes no & operator.

A borrowed parameter may inspect its value and pass another borrow to an ordinary parameter. It cannot transfer ownership into a new owning location, send the value to sink, or call drop on the borrowed owner.

This call-scoped rule is narrower than a general reference/lifetime system. Prismio 0.1 does not let a function return a reference to its parameter or store a first-class borrow for later use.

Transfer with sink

A sink parameter takes ownership. Assignment into another owned location and insertion into an owned list can also move a value.

prismio
struct Box { value: Int }
fn take(sink box: Box) -> Int { return box.value }

fn main() -> Int {
    let box = Box { value: 7 }
    take(box)
    return box.value
}

The final read is rejected as a use of a moved value.

A consuming function should use sink even if its current body only reads the value. The signature is the caller-visible ownership contract and leaves the callee free to store or destroy the value later without silently changing call-site semantics.

prismio
struct Packet { code: Int }

fn release(sink packet: Packet) -> Int {
    return packet.code
}

fn main() -> Int {
    let packet = Packet { code: 5 }
    return release(packet) - 5
}

Mutate with inout

An inout parameter is an exclusive mutable borrow. Mutations are visible to the caller, which retains ownership. Because the callee may change the value, the caller must be allowed to: the argument is a let mut binding, or an inout parameter of the caller's own.

prismio
struct Box { value: Int }
fn bump(inout box: Box) { box.value = box.value + 1 }

fn main() -> Int {
    let mut box = Box { value: 9 }
    bump(box)
    return box.value - 10
}

An inout call is synchronous and call-scoped in the current language. There is no first-class mutable-reference value that can escape the function. The compiler uses the parameter mode to reject operations inconsistent with the borrow.

Owned fields and collections

Building an aggregate transfers its move-only components into the aggregate:

prismio
struct Envelope { body: String }

let text = "payload"
let envelope = Envelope { body: text }

After construction, envelope owns the string and text is moved. Similarly, items.push(value) moves a move-only element into the Vec. items[i] is a view that follows borrowing behavior for owned elements in the current compiler; there is no general move-out iterator.

Be explicit about ownership at API boundaries. A read-only helper should accept an ordinary parameter. A state-changing helper should use inout. An operation that takes responsibility for storage or destruction should use sink.

Drop

drop(value) explicitly consumes a currently owned move-only value. Dropping a borrowed value, a copied scalar/array, or an already moved value is rejected.

prismio
struct Resource { handle: Int }

fn main() -> Int {
    let resource = Resource { handle: 3 }
    drop(resource)
    return 0
}

drop communicates early end of ownership to the compiler/runtime. It is not a destructor method dispatch, and user-defined destructor declarations are not available in 0.1.

Scope exit and cleanup

The compiler and runtime manage supported move-only values at scope exit according to current lowering and allocation-inference decisions. A value already moved or dropped must not be destroyed again.

A value produced by a call and consumed directly as an argument -- simulate(band(x)) -- has no name to hang a scope-exit release on, so it is released immediately after the enclosing call returns. That is the same point a borrow ends: the callee borrowed it for the duration of the call and the caller remained its owner throughout. Binding the result to a let first is equivalent and releases at scope exit instead.

The release is withheld where it cannot be proven safe -- when the callee may hand the argument back through its own return value, when the call crosses a foreign boundary whose contract governs the result, and for a spawned call, whose task may still be running. In those cases the value is kept rather than freed.

The experimental Adaptive Inference Framework may choose stack, region, reference-counted, or traced strategies based on analysis and annotations. These strategies are implementation mechanisms; they do not remove source-level ownership rules.

Prismio 0.1 should not yet be described as having a complete formally proven borrow checker or universal leak-freedom guarantee. Foreign code, raw pointers, experimental allocation tiers, and incomplete semantics remain boundaries where contracts matter.

Loops and conditional moves

Moving an outer binding from within a loop is rejected because later iterations could observe it as moved. Prismio 0.1 has no explicit reference syntax, borrow blocks, user-written lifetimes, or general flow-sensitive alias model.

prismio
import std.io

struct Work { id: Int }
fn consume(sink work: Work) -> Int { return work.id }

fn main() -> Int {
    let work = Work { id: 1 }
    for index in 0..<2 {
        consume(work)
        println(index)
    }
    return 0
}

Even though application logic might intend only one consuming path, the loop can execute more than once. Transfer ownership outside the loop or restructure the data so each iteration owns a distinct value.

Branch-sensitive move checking exists where supported by semantic analysis, but do not assume a fully general alias or path theorem. Prefer simple, visible ownership flows.

Foreign ownership

extern fn declarations can attach contracts such as borrow, retain, consume, out, alias, and produce(free_fn). Those contracts tell the compiler how ownership crosses an ABI boundary, but Prismio cannot verify the foreign implementation. A wrong declaration can cause leaks, use-after-free, or double destruction despite valid Prismio source.

Rules summary

  • Strings, lists, and structs are move-only; scalars, enums, raw pointer values, and arrays of a known length copy. A [T] parameter views the caller's array.
  • Ordinary parameters borrow move-only values.
  • sink transfers ownership to the callee.
  • inout grants temporary mutable access while the caller retains ownership.
  • Assignment into another owned location and list insertion can move a value.
  • drop consumes a currently owned move-only value.
  • A moved or dropped binding cannot be read.
  • An outer move-only binding cannot be consumed inside a repeating loop when later iterations could reuse it.
  • There is no source-level &, general reference type, or user-written lifetime syntax.

See the memory model for specification language and the ownership error index for repair-oriented examples.