Ownership#

A file, a socket, a C allocation — anything that needs exactly one cleanup — carries that obligation in its type. The checker finds the missing cleanup, the double free, and the use-after-move before the program runs.

The model is deliberately smaller than Rust's: no named lifetimes, no typestate, and no borrow checker over arbitrary object graphs. It is aimed at the failures that happen at a C boundary, and it costs one annotation on the producer.

This page is the working subset. The ownership reference has the complete model.

Declaring a resource#

@drop marks the operation that consumes the resource; @owned marks the function that produces one:

The annotation, rather than the name, is what makes close the drop operation. Bare @owned is accepted only when the result type has exactly one, so the compiler never has to guess between close, free, flush, and stop.

For a type you do not own, the drop operation can be a free function, and the producer names it:

A drop operation must takes its resource. That is what makes it consuming.

Discharging the obligation#

Once you bind an owner, its exact cleanup runs automatically at the binding's lexical boundary:

There are three ways to end or transfer the obligation before that boundary.

Drop it at the point you choose:

Hand it on to a parameter that takes it:

Return it from a function that is itself @owned.

Inside a function, a takes parameter is discharged by passing it to another takes parameter — the drop operation, or something that adopts it. nupp.drop() needs a value whose static type carries a cleanup list, and a bare takes binding does not have one, so nupp.drop(session) there reports NUPP2602 and names the fix.

Borrowing#

A borrows parameter gets access for the duration of the call without taking responsibility:

borrows is a lifetime and aliasing contract rather than a const qualifier — mutating through one is allowed. Use exclusive for a call that needs sole access because it may invalidate views derived from the value.

For a Nupp function with a body, the checker infers whether a resource parameter escapes, so a read-only helper needs no annotation at all. Writing borrows anyway pins the contract: a later change that stores the value errors inside the function instead of silently changing its interface and breaking callers.

A borrow may be read, mutated, and reborrowed. It may not be returned without a contract, stored in a table or field, assigned to an outer binding, or captured by a closure.

Lexical destruction#

Cleanup runs on fallthrough, return, break, continue, a goto leaving the block, and an error raised anywhere inside. Several resources are acquired left to right and released right to left.

The bindings remain owners: they may be moved, returned under an owning contract, or explicitly dropped early. Each successful transfer deactivates automatic cleanup exactly once.

Records that hold resources#

A record with owned<T> fields is itself a resource, and cleanup is synthesized in reverse field order:

A custom @drop method has to discharge every affine field, and it does that by calling their drop operation directly:

Where it stops#

unsafe do grants permission for pointer operations the checker cannot prove — raw dereference, intoRaw, fromRaw, borrowFrom. It grants nothing else: owners still have to be discharged inside one, borrows still cannot escape, and ordinary lexical cleanup still runs.

The trusted parts are written down. Whether a C function really consumes, retains, or releases a pointer comes from its declaration, because a header has no body to inspect. Whether a returned resource is genuinely exclusive is not observable from a pointer value. Cleanup bodies are not verified. Those are the auditable edges; everything inside them is checked.

Next#

  • The ownership reference — the complete model, C output parameters, pinning, and the proved-versus-trusted table.
  • C interop — parameter modes at a C boundary.