Ownership, Borrowing, and Drop
Borrowing
Borrows distinguish readonly access from readwrite access.
func inspect(file: &File): void {
...
}
func write(file: &+File, data: &str): void! {
...
}
Rules:
&Tis a readonly borrow type.&+Tis a readwrite borrow type.&valuecreates a readonly borrow.&+valuecreates a readwrite borrow.&+valuemay be created only from a writable place, such as avarbinding, a writable field, or an existing&+Treborrow.- Readonly borrows may coexist with other readonly borrows.
- A readwrite borrow is exclusive and cannot coexist with other readonly or readwrite borrows of the same value.
- A value cannot be moved while it is borrowed.
- A value cannot be explicitly dropped while it is borrowed.
let _ = expressionconsumes and drops the resulting owned value without creating a binding. Existing move-only bindings requirelet _ = move name; newly produced owned temporaries do not requiremove.- A borrow cannot outlive the value it refers to.
- A borrow of a stack value cannot escape the stack value's scope.
- A borrow of region-allocated memory cannot escape that region.
- Ordinary function calls require explicit borrow syntax at the call site.
- Method receivers may create the required borrow automatically.
- Lifetime annotations are not supported.
&+is a single lexical token.- Unary
+xis not part of the language. This avoids ambiguity with&+x.
Examples:
var file = File.open(path)?
let a = &file
let b = &file // OK: multiple readonly borrows
let c = &+file // error: a and b are used below
inspect(a)
inspect(b)
var file = File.open(path)?
let w = &+file
drop file // error: w is used below
write_more(w)
Ordinary function calls are explicit:
func inspect(file: &File): void {
...
}
inspect(&file)
Method receiver borrows are automatic:
instance File {
pub method &+self.write_text(text: &str): void! {
...
}
}
file.write_text("hello")?
The method call above creates a temporary readwrite borrow of file for the call. This does not enable UFCS-style calls:
File.write_text(&+file, "hello") // error
A newly created owned temporary may be used as a readwrite receiver for one method call:
(File.open(path)?).write_text("hello")?
The temporary receiver is dropped according to the statement-end temporary rules in Control Flow.
Borrow Checker
Nocter uses source-level non-lexical borrow ranges.
A borrow is active from the expression that creates it through the last source-level use of the borrow-like value derived from it. The borrow may end before the lexical scope of the borrow binding ends if the binding is not used again.
let read = &file
inspect(read)
let write = &+file // OK: read is not used after inspect(read)
let read = &file
let write = &+file // error: read is used below
inspect(read)
Rules:
- Source-level lifetime annotations are not supported.
- The compiler determines borrow live ranges from actual source uses, not only from lexical scopes.
- A use includes passing the borrow-like value to a call, method call, field access, index access, return, assignment, initialization, storing it inside an aggregate, or deriving another borrow-like value from it.
- Scope end of a plain borrow-like value does not by itself extend the borrow, because borrow-like values are non-owning and do not run
drop. - A readonly borrow may overlap with other readonly borrows of the same place.
- A readwrite borrow must not overlap with any other readonly or readwrite borrow of the same place.
- Borrow overlap is determined from semantic places and projections, not from runtime numeric addresses. Distinct zero-sized places remain distinct for ownership and exclusivity even when their borrows use the same machine address.
move place,drop name, whole-place assignment, field assignment, and reinitialization are invalid when they conflict with an active borrow.- A borrow cannot outlive the storage it refers to.
- A borrow cannot outlive a region from which its storage was allocated.
- A borrow or borrow-like value derived from a temporary owned value cannot escape the statement that owns the temporary.
- A borrow-like result discarded by
let _ = expressiondoes not extend the source borrow beyond the discard statement. - A borrow held only by a temporary in an ordinary
iforwhileboolean condition ends when the condition temporary is dropped, before the selected body begins. Retaining the borrow through the body requires an explicit longer-lived binding. - Method receiver borrows last only for the call unless the method returns a borrow-like value derived from the receiver.
- If a method returns a borrow-like value derived from the receiver, the receiver borrow remains active for the returned value's live range.
&str,&[T],&+[T],ViewIter<T>, and aggregates containing borrow-like values participate in the same live-range and provenance checks.- Payload borrows introduced by
matchandif expr is Patternare projections of the pattern target borrow. They retain its readonly or readwrite capability and provenance through the last use of any derived payload borrow. - Borrow-like return values are governed by Borrow-like Return Values.
Field-Sensitive Borrows
Nocter tracks disjoint named struct fields for simple places.
Field-sensitive tracking applies only to direct named field paths whose base is a local binding, parameter binding, or borrow binding that the compiler can resolve statically.
var user = User {
name: String.copy("alice"),
count: 0,
}
let name = &user.name
user.count += 1 // OK: count is disjoint from name
inspect(name)
Rules:
- A borrow of a whole value conflicts with borrows and mutations of any field of that value.
- A borrow of one named field does not conflict with mutation or borrowing of a disjoint named field.
- Moving, dropping, reinitializing, or assigning the whole parent value conflicts with any active field borrow.
- Assigning a field conflicts with active borrows of that same field and active borrows of the whole parent value.
- Field-sensitive tracking does not apply to array indexes, collection indexes,
&[T]or&+[T]elements, pointer dereferences, method-call results, enum payloads, or computed projections. - If the compiler cannot prove two places are disjoint, it treats them as conflicting.
Function Parameters
Parameters are immutable bindings inside the function body.
func create(name: String, count: i32, out: &+File): User! {
out.write_text(&name as &str)?
return User {
name: move name,
count: count,
}
}
Rules:
- Parameters are immutable bindings.
- Parameter bindings cannot be reassigned.
- Mutable parameter bindings are not supported.
- Parameter names must be unique within the parameter list.
- Parameter shadowing is not allowed, following the normal name-resolution rules.
- An owned parameter is owned by the function body.
- A move-only owned parameter is dropped at function scope end unless it is moved.
- Moving a move-only parameter requires
move parameter. - After a move-only parameter is moved, that parameter binding is no longer valid.
- A copy parameter may be copied by ordinary use.
&Tparameters are readonly borrow bindings.&+Tparameters are readwrite borrow bindings.- A borrow parameter does not own the referenced value and does not drop it at function scope end.
- The
&+Tparameter binding itself cannot be reassigned, but the referenced value may be mutated through it. - Method receivers are explicit parameters and follow the same binding, ownership, and borrow rules.
- Default parameters and named parameters are not supported.
Examples:
func rename(user: &+User, name: String): void {
user.name = move name
}
func invalid_reassign(name: String): void {
name = String.empty() // error: parameters are immutable bindings
}
func normalize(value: i32): i32 {
var current = value
if current < 0 {
current = -current
}
return current
}
func increment(value: &+Counter): void {
value.count += 1 // OK: mutates the referenced Counter
}
func invalid_rebind(value: &+Counter, other: &+Counter): void {
value = other // error: parameter binding is immutable
}
Drop
Resource destruction uses an independent top-level drop declaration, not an instance method or a Drop interface.
drop File(&+self) {
close(self)
return
}
drop is an identifier token. The parser recognizes it contextually at the start of a top-level drop declaration and at the start of an explicit drop value statement. Outside those two source forms, drop is an ordinary identifier.
The declaration and statement source forms are defined by Drop and Test Declarations and the common statement grammar.
A declaration such as func drop(...) or method &self.drop(...) declares an ordinary function or method named drop. It does not define destruction behavior.
Rules:
- A nominal type family may define at most one drop declaration.
- A drop declaration has the source form
drop TypePattern(&+self) { ... }. - Its target must be an ordinary
structor a payload-bearingenumdeclared in the same module. Type aliases,copy structfamilies, payloadless enums, and non-nominal types cannot own a drop declaration. selfis the fixed drop receiver name and is scoped to the drop body.- The drop receiver type is always exactly
&+Self. - A drop declaration is top-level.
instancecontains inherent methods and bodyless interface implementations. A drop declaration cannot appear inside an instance body. - A drop declaration has no visibility, target directive, generic-prefix,
where, or return-type annotation. - Its target pattern must cover every generic slot exactly once with a distinct binder.
- The declaration applies uniformly to every specialization of its nominal type family.
- Every eligible target family is move-only by declaration. Copyability is determined from the type declaration and substituted fields, never changed by the presence or absence of a drop declaration.
- A drop body always returns no value and cannot be fallible.
- A drop declaration cannot be called as a normal construction function or method.
file.drop()is an ordinary method call if an ordinary method nameddropexists; it does not invoke the drop declaration.File.drop(&+file)is an ordinary construction-function call only ifconstruct Filedeclares a construction function nameddrop; it does not invoke the drop declaration.- A drop body cannot report cleanup failure through a fallible return.
- If an operation inside
dropcan fail, thedropbody must ignore that failure, record it in already-owned state before destruction, or terminate withtrap/abort. - Terminating with
traporabortfrom insidedropdoes not unwind remaining caller scopes. - Owned values are automatically dropped at scope end.
- Initialized owned values are dropped in reverse declaration order.
- Struct drop glue invokes the struct's own drop declaration first when present, then drops owned fields in reverse declaration order. Payload-bearing enum drop glue invokes the enum's own drop declaration first when present, then drops only the active variant payload in reverse payload declaration order. Field and payload drop glue follows the same rule recursively.
- Consuming enum patterns preserve this ordering through their dedicated rule in Enums and Variant Construction: a drop body that would otherwise receive partial storage runs once before a named move-only payload leaves, while later residual cleanup drops only the still-initialized payload fields.
- Maybe initialized owned values use compiler-generated conditional drop.
- Uninitialized bindings are not dropped.
returnand postfix?propagation run the same scope-end drop behavior, including conditional drop.- A moved value is not dropped through the original binding.
Valid move-only type:
struct File {
handle: usize
}
drop File(&+self) {
close(self.handle)
return
}
A generic drop declaration covers the complete family and cannot be refined:
struct Buffer<T> {
value: T
}
drop Buffer<T>(&+self) {
release(self)
return
}
Copyable targets are invalid, including an enum whose variants all carry no payload:
copy struct Point {
x: i32
y: i32
}
drop Point(&+self) { // error: Point is copyable
return
}
enum Token {
only
}
drop Token(&+self) { // error: Token is copyable
return
}
A generic copy struct cannot own a drop declaration. The compiler does not search substitutions or turn only its non-copy specializations into separately destructible types:
copy struct Box<T> {
value: T
}
drop Box<T>(&+self) { // error: copy struct families cannot own drop declarations
return
}
Explicit Drop Statements
Explicit early destruction uses a drop statement.
var file = File.open(path)?
drop file
After drop file, the binding enters an uninitialized state.
file.read() // error
Rules:
drop nameis a statement.- The operand of
dropmust be a local binding name or parameter binding name. dropis not a reserved keyword and is not an ordinary function call in this statement form.- The operand must be initialized.
- The operand must be a move-only owned binding.
- Copy types cannot be explicitly dropped.
- Borrow bindings such as
&Tand&+Tcannot be explicitly dropped because they do not own the referenced value. - Maybe initialized bindings cannot be explicitly dropped.
- Uninitialized bindings cannot be explicitly dropped.
- A binding cannot be explicitly dropped while it is borrowed.
drop nameruns the same drop glue as scope-end automatic drop.dropis not fallible.dropproduces no value.- After
drop name, the binding is uninitialized on all later reachable paths. - A dropped
varbinding may be reinitialized by assigning to the whole binding. The detailed rules are specified in Bindings and Initialization. - A dropped
letbinding cannot be reinitialized. drop object.field,drop array[index], anddrop make_value()are invalid.
Examples:
var file = File.open(path)?
drop file
file = File.open(other)?
file.read()?
Invalid:
drop count
drop ref
drop object.field
drop array[index]
drop make_value()
Copy and Move
Types are move-only by default. Only copy types may be copied implicitly.
Copyable structs are declared with copy struct.
copy struct Point {
pub x: i32
pub y: i32
}
Rules:
- Types are move-only by default.
copy structopts a nominal family into structural copyability. An ordinarystructremains move-only even when all its fields happen to be copyable.- A non-generic
copy structis copyable only when every field is copyable; a declaration with an unconditionally move-only field is invalid. - A generic
copy structderives one copy condition from its fields. A field whose copyability depends on a generic parameter contributes that dependency. A field that remains move-only for every substitution makes the declaration invalid rather than defining a misleading never-copycopy structfamily. - After concrete substitution, a valid generic
copy structspecialization is copyable exactly when every substituted field type is copyable. For example,copy struct Box<T> { value: T }is copyable asBox<i32>but move-only asBox<String>, while a field such as&Tremains copyable for everyT. - No copyable type can own or acquire a drop declaration. This includes primitive numeric types,
bool, raw pointers, payloadless enums, copyable fixed arrays, copyablecopy structspecializations, copyable borrows, and aliases to any of these types. - Because one drop declaration covers a complete nominal type family, every
copy structfamily is ineligible even when a particular specialization is move-only. A drop declaration never changes a type from copyable to move-only. - A copyable
copy structspecialization cannot own a field that requires destruction. A conditional specialization such asBox<String>is move-only and runs ordinary structural field cleanup; thecopy structfamily itself still cannot declare a type-owned drop body. - Primitive numeric types,
bool, and raw pointers are copyable. - The built-in
errortype is move-only and has compiler-defined destruction. - Payloadless enum values are copyable.
- Fixed-size arrays
[T; N]are copyable whenTis copyable. - An optional
T?is copyable exactly whenTis copyable. - Every fallible
T!is move-only, regardless of its success payload. The special payloadless-success typevoid!is also move-only. - Supported mixed outcomes containing a fallible layer are move-only. An optional
T?remains copyable exactly whenTis copyable. - Copyability is a property of the complete outcome type, not its currently active tag. An absent
String?and a failedString!remain move-only because another value of the same type may own aStringpayload. - A closure's anonymous environment is copyable exactly when every stored capture is copyable. A capture-free closure and a closure containing only readonly-borrow captures are copyable. A closure containing a readwrite-borrow capture or move-only owned capture is move-only.
- Callable capability and closure copyability are independent.
&func,&+func, andfuncdescribe how a closure may be invoked; they never change whether its environment may be copied. - Type aliases to copy types are copyable. For example, a project-local alias to
i32is copyable. &Tis copyable.&+Tis not copyable.- A generic parameter is treated as potentially move-only unless its declaration or enclosing
callable contract requires
copy. copy Tpermits ordinary copy operations in the generic body and is checked again against every concrete substitution. It adds no runtime metadata and cannot be implemented by user code.- Non-copy values are not implicitly moved by assignment, argument passing, or return.
- Moving a non-copy value requires explicit
move. - Direct collection iteration follows the same rule. An existing move-only iterator binding must
be written as
for item in move iterator; only a newly produced iterator temporary or a copyable iterator may appear as a bare direct-iterator source.
Examples:
let p1 = Point { x: 1, y: 2 }
let p2 = p1 // OK: Point is copy
let text1 = String.new()
let text2 = text1 // error: String is not copy
let text3 = move text1 // OK
Generic-dependent copyability is valid:
copy struct Box<T> {
value: T
}
let number_box: Box<i32> = Box { value: 1 } // copyable
let text_box: Box<String> = Box { value: text } // move-only
A field that is unconditionally move-only makes the declaration invalid:
copy struct Invalid<T> {
text: String // error: move-only for every T
marker: &T
}
Copyable optionals copy like other copy types:
let maybe: i32? = find_count()
let copied = maybe
inspect(maybe) // valid: maybe remains initialized
An optional that can contain a move-only payload is move-only even when absent. Every fallible outcome is move-only because failure owns an error:
let maybe_name: String? = find_name()
let copied_name = maybe_name // error: String? is move-only
let moved_name = move maybe_name
let read_name: String! = load_name()
let copied_read = read_name // error: String! is move-only
let moved_read = move read_name
let completion: void! = flush()
let copied_completion = completion // error: void! is move-only
let moved_completion = move completion
Function calls follow the same rule.
func consume(text: String): void {
...
}
let text = String.new()
consume(text) // error
consume(move text) // OK
Static opaque results written as some Interface are move-only at the public boundary. A caller cannot infer copyability from the hidden witness. Explicit move transfers an existing opaque binding, and normal scope, return, optional, fallible, and path-sensitive cleanup destroys the hidden value exactly once.
Move Expressions
move is a unary expression that explicitly transfers ownership from a binding or named struct field.
let b = move a
consume(move text)
consume(move pair.second)
return move value
let item = move maybe?
Move syntax uses the place-specific production under Expression Precedence.
An outcome suffix is applied to the completed move expression. Therefore move maybe? is equivalent to (move maybe)?, not move (maybe?).
Rules:
moveis a reserved keyword, not an ordinary function.move placeis an expression.- The operand of
movemust be a local binding, parameter binding, or a named struct field rooted in one of those bindings. - The root must own the selected storage: an owned local, owned parameter, or owned closure capture. A readonly or readwrite borrow binding, borrowed closure capture, or field path that crosses a borrow cannot be a move root. Readwrite permission permits mutation, not extraction of the caller's ownership.
- The operand binding may be immutable or mutable.
- The selected place must have a move-only type.
- Using
moveon a copy type is a compile error. move placehas the same type as the selected place.- Evaluating
move placetransfers ownership out of that place. move place?andmove place!first transfer the complete optional or fallible value and then apply their one outcome suffix.move place catch ...andmove place otherwise ...likewise move the place before the lower-precedence eliminator.- A second outcome layer requires a new expression boundary, as in
(move result?)?. The parentheses are semantically relevant grammar and are not removed as redundant grouping. - After a whole-binding move, the binding is uninitialized on all later reachable paths. After a named-field move, that field is uninitialized and the parent is partially initialized; disjoint initialized fields remain usable and retain their own cleanup obligations.
- A moved
varbinding may be reinitialized by assigning to the whole binding. The detailed rules are specified in Bindings and Initialization. - A moved
letbinding cannot be reinitialized. - Moved storage is not dropped through its original place.
- A place cannot be moved while it conflicts with an active borrow.
movedescribes ownership transfer. It does not specify whether generated code copies bytes, passes a pointer, or elides a copy.- Moving a newly constructed value is unnecessary and invalid.
- This restriction also applies under collection iteration and sequence spread.
moveinfor item in move sourceand...move sourceis the ordinary place-only move expression, not a contextual ownership selector for a newly produced value. - Moving from an index, dereference, call result, postfix
?expression, conditional expression, or parenthesized complex expression is invalid. - The valid
move place?spelling does not contradict that restriction: the move operand is the place, while?applies afterward.move (place?)still attempts to move a computed result and is invalid. - Partial moves are field-sensitive only for statically named struct fields. Index moves from arrays or collections, enum-payload moves, and computed projections are not supported.
- A named-field move is invalid when any proper-prefix struct made partially initialized by that
move has its own drop declaration. Drop bodies always receive one complete initialized
Self; the compiler neither calls one on a partial value nor silently omits it. match move placeandif move place is Patternmove the complete enum place before selecting a branch. This is not a partial enum-payload move; branch payload bindings receive ownership from the already consumed enum according to the enum pattern rules.
Valid:
let b = move a
consume(move text)
consume(move pair.second)
return move value
user.name = move name
let item = move maybe?
Invalid:
move make_value()
move (make_value()?)
move (maybe?)
move (condition ? a : b)
move array[index]
move copy_value
Assignment may replace a live move-only field when the right-hand side produces a complete replacement value. The replacement is evaluated first, then the old field is dropped, and ownership of the replacement transfers into the field. Moving a named field records that field as separately dead. Automatic cleanup destroys only the remaining initialized fields, in their ordinary reverse declaration order. The whole parent cannot be used or moved while it remains partially initialized. A mutable parent may restore a moved field through field assignment before a later whole-value use.
Control-flow joins merge named-field state independently. A field initialized on every incoming path is initialized, a field uninitialized on every incoming path is uninitialized, and a mixture is maybe initialized. Assignment to an uninitialized or maybe initialized field of a var parent restores that field through the common no-drop or conditional-drop assignment rule. Scope exit and whole-parent replacement likewise drop only live fields, using conditional drop for maybe initialized fields.
This partial state exists only for structs without their own drop declaration. A field whose type has a drop declaration may still be moved as one complete field; its drop obligation transfers to the destination. The prohibition concerns each enclosing struct that would otherwise require a drop body to observe an incomplete Self.
func rename(user: User, name: String): User {
var next = move user
next.name = move name
return move next
}
Invalid partial move through a type-owned drop contract:
struct Session {
socket: Socket
label: String
}
drop Session(&+self) {
record_session_end(self)
return
}
let session = open_session()
let socket = move session.socket // error: Session owns a drop declaration
Conditional field restoration:
var user = make_user()
if condition {
consume(move user.name)
}
user.name = String.copy("replacement")
use(user) // valid: every field is initialized again
Return Values
Returning an existing move-only place requires explicit move.
Rules:
return valuemay return a copy value by copying it.return placeis invalid whenplaceis an existing move-only binding or named field.return move placereturns an existing move-only binding or eligible named struct field by moving the commonMovePlacedefined above.return move placetransfers the selected place before return cleanup. A whole binding becomes uninitialized; a named field leaves its eligible parent partial, and return cleanup drops only the remaining initialized fields.- A newly constructed owned value may be returned with
return exprwithoutmove. - Newly constructed owned values include struct literals, enum variant constructors, array literals, and function or method call results.
returnevaluates the returned expression first.- When control leaves through
return, the returned value is not dropped by the callee. - Other live local owned values are dropped in reverse declaration order.
- Moved bindings are not dropped.
- Copy parameters may be returned with
return parameter. - Move-only owned parameters require
return move parameter. - Optional and fallible return layers are constructed by recursive outcome injection.
- Injection transfers or copies the recursively accepted value into the active payload exactly
once. It does not copy a move-only value or relax the explicit-
moverequirement. - Cleanup owns only the active payload of the constructed outcome. A returned active payload is not also dropped as a callee local.
- Bare
returnis valid forvoidand for successful completion ofvoid!.
Examples:
func make_text(): String {
let text = String.new()
return move text
}
func make_user(name: String): User {
return User {
name: move name,
}
}
func take_user(user: User): User {
return move user
}
func invalid(user: User): User {
return user // error: User is move-only
}
Borrow-like Return Values
Borrow-like return values are allowed only when the compiler can prove the referenced storage lives after the function returns.
Borrow-like return values include:
&T&+T&str&[T]&+[T]ViewIter<T>- structs, enums, optionals, fallible values, and arrays containing borrow-like values
The detailed provenance source kinds are specified in Strings, Arrays, Views, and Pointers.
Rules:
- Borrow-like return values must carry provenance to storage that outlives the function call.
- Borrow-like values derived from static storage, such as string literals, may be returned.
- Borrow-like values derived from input borrow-like parameters may be returned when the return value's provenance is still tied to that input borrow-like value.
- A readonly borrow-like value may be returned from a readonly or readwrite input borrow-like source.
- A readwrite borrow-like value may be returned only from a readwrite input borrow-like source, such as
&+Tor&+[T]. - Borrow-like values derived from local owned values cannot be returned.
- Borrow-like values derived from temporary owned values cannot be returned.
- Borrow-like values derived from owned parameters cannot be returned, because owned parameters are dropped at function scope end unless moved.
- Borrow-like values derived from region-allocated storage cannot escape the region.
- Nocter has no source-level lifetime parameters or lifetime annotations.
- If provenance cannot be proven by the compiler, returning the borrow-like value is a compile error.
Examples:
func greeting(): &str {
return "hello" // OK: string literal storage is static
}
func first_byte(bytes: &[u8]): u8? {
if bytes.len() == 0 {
return none
}
return bytes[0] // OK: u8 is copy
}
func bad(): &str {
var text = String.copy("hello")
return &text as &str // error: view points to local owned value
}
func also_bad(text: String): &str {
return &text as &str // error: view points to an owned parameter dropped at return
}