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

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.