Ownership and Memory Safety
seed uses ownership, move-only resources, lexical loans/provenance, structural
capabilities, deterministic cleanup, linear t, and regions. It has no GC,
borrow-checker lifetime syntax, or exception unwinding. The authoritative
semantic rules are in seed/compiler/llvm/SEMANTIC_RULES.md; current limits
are tracked in Current Status.
Copy, move, borrow, clone
| Operation | Meaning |
|---|---|
| copy | source and destination remain available; only for copy-capable values |
| move | ownership transfers and the source place becomes unavailable |
| borrow | a zero-copy view is tied to proven backing storage |
| clone | explicit independent value/handle according to structural capability |
Scalars and aggregates containing only copy fields copy. string, bytes,
shared_bytes, user-destructor owners, resource aggregates, and linear t
obey their move/clone/cleanup capabilities.
shared_bytes is not bitwise-copyable: .clone() explicitly retains another
handle without copying the payload. Cloning owned string/bytes or aggregates
containing them may allocate. Clone allocation effects and capabilities survive
source-elided .sdi compilation.
Places and partial ownership
The compiler tracks locals, fields and nested fields, known fixed-array indexes, active ADT payloads, raw-union fields, tuple/array/nominal aggregates, generic instances, option/result, function values, channels/tasks, aliases, blocks, calls, returns, and control-flow joins.
A partial move makes the moved place unavailable and prevents whole-owner use when completeness is required. Independent remaining fields stay usable when a user destructor does not require a complete owner. Dynamic-index moves of resource elements remain rejected because the cleanup set would be runtime dependent.
Cleanup
Cleanup is planned from typed HIR ownership facts, verified separately, and lowered onto normal structured exits. Live owners clean exactly once in reverse successful initialization order. Reassignment cleans the previous live owner before storing the replacement. Loops use dataflow fixed points across entry, back edges, break, continue, and return.
Panic, trap, and abort terminate without unwinding. They do not run lexical cleanup or user destructors.
Destructors
fn type_name.drop() receives implicit consuming self. It must be unique and
return void. Calling value.drop() manually consumes the owner, preventing a
second automatic call. Returned owners transfer their cleanup obligation to the
caller. Cleanup/resource globals are rejected because global teardown is not an
implicit language service.
ADT cleanup dispatches only over the active variant. Raw unions require unsafe active-field discipline; safe tagged alternatives should use ADTs or an audited wrapper.
Closure ownership
A plain |...| closure owns compiler-generated stack capture storage. Capture
initialization follows first lexical-use order: copy-capable values copy and
other owned values move. Captures are immutable; a moved resource capture makes
the closure itself move-only. The owner drops live captures exactly once in
reverse initialization order.
Plain closures are reusable and non-escaping. They cannot capture values that
cannot remain safely stored after a call, including views, mutable captures,
region-bound values, raw/reference values, format_args, or already-moved
places. They cannot be returned, retained in an outer scope, sent to a task, or
passed through a platform function-pointer boundary.
box |...| allocates capture storage explicitly and returns
result<box fn(...) -> t, alloc_error>. The boxed owner can be returned or
stored but remains move-only, non-cloneable, and non-sendable. Allocation
failure drops transferred captures once; successful drop runs the capture drop
thunk and frees the environment once. A boxed closure allocated under
region(...) cannot escape that region.
Linear values
linear t preserves t's representation and adds an exactly-once obligation.
It removes copy/clone/shared capability while retaining valid move, borrow,
send, sync, and cleanup facts. The implementation propagates the obligation
through aliases, blocks, aggregates, option/result, ADTs, generics, channels,
function values and indirect calls, task capture, parameters/returns, and
schema-16 .sdi.
Leaving a linear value live at scope exit, consuming it twice, cloning it, discarding it, overwriting it, or storing it into pre-existing non-linear storage is rejected.
Text, buffers, and views
- literal text is static borrowed
str; stringis owned valid UTF-8 and moves;bytesis a unique mutable binary owner and moves;shared_bytesis a shared read-only handle with explicit clone;- owned
stringautomatically borrows asstrwhen expected; - owner-derived
str,[]t, andmut []tviews carry provenance; - a live view prevents conflicting move, overwrite, free, reallocation, or mutation of its backing owner;
mut []t.split_at_mut(index)checksindex <= len, allocates nothing, and returns two exclusive lexical views;matchbindings may usemut name;- the backing owner cannot be read, moved, resized, or freed while the partition tuple is live; nested partitions remain confined to their parent;
- returning or storing a view is allowed only when its static/input/receiver provenance proves sufficient lifetime;
- local/temporary-derived views cannot escape through returns, globals, containers, tasks, or dynamic-library ABI.
There is no lifetime syntax. @borrow receiver methods create shared lexical
loans; returned views remain tied to the receiver through .sdi summaries.
@mut receiver methods create an exclusive call-scoped loan and mutate the
caller's place directly. Immutable receivers and overlapping live views are
rejected; no move or clone is hidden by the call.
Regions
fn work() -> result<i64, alloc_error> {
region(4096) {
// allocating operations use the active checked bump allocator
result.ok(42)
}
}
Capacity is evaluated once, aligned allocation is overflow checked, and
exhaustion is returned as alloc_error by fallible APIs. Nested regions are
LIFO. Body resources clean before the backing storage is released. Safe owners,
views, and pointers tied to a region cannot escape through result, return, or
assignment to an outer binding.
Spawned work captures allocator context. Region-bound tasks remain structured and join before region cleanup.
Task transfer
spawn records copy/move capture mode. Keeping the original and a task value
requires an explicit clone. Crossing a task boundary requires send; shared
access also requires sync. Mutable global state cannot be captured. Each task
group drains before its lexical owner cleans resources.
Unsafe boundary
Raw allocation, pointer access, FFI, syscalls, atomics, volatile access,
address-space casts, raw union access, and raw assembly require unsafe.
Unsafe code does not disable ownership for surrounding safe values; it assumes
specific obligations that should be hidden behind narrow audited safe wrappers.
unsafe struct parameters: an unsafe struct with a raw pointer field
and @repr("null_niche") is a resource type that cannot be passed by value
as a function parameter. Passing it by value moves the underlying owner,
making it unavailable for subsequent use. Methods with @mut receiver require
a mutable place and cannot be called on a moved value. To use such a type
across function boundaries, inline the logic or return the owner alongside the
result.
What release changes
--release and --release-small optimize the same safe semantics. Bounds,
discriminant, provenance, ownership, and cleanup checks remain unless proven
redundant. No unchecked release profile is implemented.