Ownership, Borrowing, and Drop
This file is part of the Nocter language specification. The specification entry point is README.md.
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.
- 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 part of the initial design.
&+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:
impl 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 v0
Adopted: v0 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 part of v0.
- 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.
move name,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.
- 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.- Borrow-like return values are governed by Borrow-like Return Values.
Field-Sensitive Borrows
Adopted: v0 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(allocator, "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 in v0. - If the compiler cannot prove two places are disjoint, it treats them as conflicting.
Function Parameters
Adopted: parameters are immutable bindings inside the function body.
func create(name: String, count: i32, out: &+File): User! {
out.write_text(name.view())?
return User{
name: move name,
count: count,
}
}
Rules:
- Parameters are immutable bindings.
- Parameter bindings cannot be reassigned.
varparameters are not part of v0.- 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 part of v0.
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
Adopted: resource destruction uses a dedicated drop member inside an inherent impl, not a Drop trait.
use std/os as os
impl File {
drop &+self {
os.close(self.fd).ignore()
}
}
The destructor member is the contextual drop &+self { ... } member form. drop is not a reserved keyword. The lexer emits drop as an identifier token, and the parser recognizes the two exact source forms for destruction: a destructor member inside an inherent impl block, and an explicit drop name statement. Outside those forms, drop is an ordinary identifier.
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 type may define at most one
dropmember. - A
dropmember has the source formdrop &+self { ... }. selfis the fixed drop receiver name and is scoped to the drop body.- The drop receiver type is always exactly
&+Self. - A
dropmember can appear only in an inherentimpl Typeblock. Interface conformance declarations cannot containdropmembers or method bodies. - A
dropmember has no return type annotation. - A
dropmember always returns no value. - A
dropmember cannot be fallible. - A
dropmember cannot be markedpub. - A destructor member cannot be called as a normal associated function or method.
file.drop()is an ordinary method call if an ordinary method nameddropexists; it is not a destructor call.File.drop(&+file)is an ordinary associated function call if an ordinary function nameddropexists; it is not a destructor call.- A
dropmember cannot report cleanup failure through 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.
- 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.
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.- In v0, 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 Values and Types. - A dropped
letbinding cannot be reinitialized. drop object.field,drop array[index], anddrop make_value()are not part of v0.
Examples:
var file = File.open(path)?
drop file
file = File.open(other)?
file.read()?
Invalid in v0:
drop count
drop ref
drop object.field
drop array[index]
drop make_value()
Copy and Move
Adopted: 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 structtypes are implicitly copyable.- Every field of a
copy structmust be copyable. - A generic
copy structis copyable per concrete instantiation. After substituting type arguments into the fields, every concrete field type must be 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 anyT. - A
copy structcannot definedrop. - A
copy structmust not own resources that require destruction. - Primitive numeric types,
bool, and raw pointers are copyable. - The built-in
errortype is copyable. - Payloadless enum values are copyable.
- Fixed-size arrays
[T; N]are copyable whenTis copyable. - Type aliases to copy types are copyable. For example, a project-local alias to
i32is copyable. &Tis copyable.&+Tis not copyable.- Non-copy values are not implicitly moved by assignment, argument passing, or return.
- Moving a non-copy value requires explicit
move.
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
Function calls follow the same rule.
func consume(text: String): void {
...
}
let text = String.new()
consume(text) // error
consume(move text) // OK
Move Expressions
Adopted: move is a unary expression that explicitly transfers ownership from a binding.
let b = move a
consume(move text)
return move value
Rules:
moveis a reserved keyword, not an ordinary function.move nameis an expression.- In v0, the operand of
movemust be a local binding name or parameter binding name. - The operand binding may be immutable or mutable.
- The operand binding must have a move-only type.
- Using
moveon a copy type is a compile error in v0. move namehas the same type asname.- Evaluating
move nametransfers ownership out of that binding. - After
move name, the binding enters an uninitialized state on all later reachable paths. - A moved
varbinding may be reinitialized by assigning to the whole binding. The detailed rules are specified in Values and Types. - A moved
letbinding cannot be reinitialized. - A moved binding is not dropped through its original binding.
- A binding cannot be moved while it is borrowed.
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 in v0.
- Moving from a field, index, dereference, call result, postfix
?expression, conditional expression, or parenthesized complex expression is not part of v0. - Partial move from a struct field is not part of v0.
- Index move from an array or collection is not part of v0.
Valid in v0:
let b = move a
consume(move text)
return move value
user.name = move name
Invalid in v0:
move make_value()
move (make_value()?)
move (condition ? a : b)
move object.field
move array[index]
move copy_value
To change one field of a move-only struct, move the whole value into a new binding, mutate that binding, then return or pass the whole value.
func rename(user: User, name: String): User {
var next = move user
next.name = move name
return move next
}
Return Values
Adopted: returning an existing move-only binding requires explicit move.
Rules:
return valuemay return a copy value by copying it.return valueis invalid whenvalueis an existing move-only binding.return move valuereturns an existing move-only binding by moving it. In v0,valuemust be a binding name.- After
return move value, that binding is no longer valid on any remaining reachable path. - 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. return noneis valid for optional return typeT?and for fallible optional return typeT?!, where it returns success absence.- Bare
returnis valid only forvoidreturn type.
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
Adopted: v0 allows borrow-like return values 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.
- v0 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(allocator: &+Allocator): &str! {
var text = String.copy(allocator, "hello")?
return text.view() // error: view points to local owned value
}
func also_bad(text: String): &str {
return text.view() // error: view points to an owned parameter dropped at return
}