Shared by default
Recoil tracks ownership statically, but the native managed series — string!, block!, vector! and array! (with their #flexible forms) — are reference-counted and shared by default. Assigning or passing one adds a reference; the original binding stays usable.
text: "hello"
alias: text ; adds a reference
print alias
print text ; OK — both bindings share the same value
Copy-like scalars (integers, floats, logic!, char!, enum!) are duplicated. Other owned values stay move-sensitive: dicts and maps, ports, errors, binary! and foreign pointer-like values are consumed by an owned transfer. Sum types inherit move sensitivity from their variant fields. The exact move-sensitive set is decided by the type checker.
#unique: opt back into move-only
Add #unique to a native-series type to make it move-only. An owned transfer consumes it:
text: make [#unique string!] "hello"
owned: text
print owned
print text ; rejected: 'text' used after move (B3000)
Passing a shared value to a #unique parameter is rejected with T4017, because that parameter requires exclusive ownership.
Stack vectors
A fixed vector! [#stack T N] is move-only without an outer #unique: sharing its handle would erase the lifetime of its function-local inline storage. A direct move into a new local is allowed. Return, capture, module storage, longer-lived reassignment and lifetime-bearing derived views are rejected with B3011. copy produces a normal heap-backed vector whose lifetime is independent of the stack owner.
Ownership state transitions
The checker tracks every binding as owned, moved or borrowed.
| Situation | Effect |
|---|---|
| Assignment of a copy type | source remains usable |
| Assignment of a share-by-default series | source stays live; reference count is incremented |
Assignment of a move-sensitive type or a #unique series | source becomes moved, destination becomes owned |
Passing :a to a borrowed parameter | a remains owned |
Passing a to an owned #unique / move parameter | a becomes moved |
| Reassignment after a move | state is restored to owned |
A move in any either branch | conservatively treated as moved afterwards |
| Use after move | B3000 at compile time |
make slice! at series offset length | source stays owned; the slice is a borrow/view |
The either rule surprises people: a move on the branch that did not run still marks the value moved afterwards, because the checker does not know which branch was taken.
Borrowing
Borrowed parameters use get-word syntax at both the function boundary and the call site:
show-name: func [
:name [string!]
return: [none!]
] [
print name
]
msg: "hello"
show-name :msg
print msg
Borrowing does not transfer ownership, and it does not imply mutation rights. print is itself borrowing, so printing a binding never consumes it.
Dictionary iteration
foreach [key value] entries reads entries owned by the dictionary. Scalars may be copied and shared strings may acquire a retained owner. Move-sensitive entries such as errors and ports stay borrowed: returning one, assigning it to an owning binding, passing it to an owning parameter, storing it in another aggregate, or closing a borrowed port is rejected with B3004.
Mutation
Prefer expression-shaped code: let a branch or a small helper return the new value instead of mutating a binding across many statements. Reach for mutation when the program needs in-place effects — updating an aggregate slot, advancing a state machine, filling a buffer, or calling an FFI API that writes through an address.
count: mutable 0
count: count + 1
state: mutable make [#mutable State!] State!/idle
state/count: state/count + 1
mutable is only valid as the direct initializer in name: mutable value (P1725). For aggregates, mutability is a type attribute on the value being constructed. Keep it local: make the smallest binding or value mutable, return the changed value instead of relying on ambient writable state, and keep borrowed views from escaping a mutation scope.
Type attributes
Attributes belong in the type spec, usually inside make [...], when the value needs a capability or storage mode beyond the inferred default. Plain locals need none.
| Attribute | Meaning |
|---|---|
#mutable | in-place slot, field or path updates |
#flexible | a native series that can grow or shrink; also mutable for existing slots |
#unique | opt a native series out of share-by-default: move-only |
#atomic | scalar storage with indivisible reads, writes and read-modify-write (see Concurrency) |
#heap | managed heap storage for a fixed vector! (the default) |
#stack | fixed inline storage for a direct function-local vector!; move-only, non-escaping |
buf: mutable make [#mutable string!] "hello"
buf/0: #"H"
grow: make [#flexible [string! 16]] none
append grow "hello"
items: make [#flexible block!] []
append items 42
A #flexible series auto-expands, so the bare form [#flexible string!] means exactly the same as [#flexible [string!]]. The inner block only exists to preset a capacity ([string! 16]) or name an element type ([vector! [i32!]]). #mutable and #flexible are different capabilities: use #mutable when the shape is fixed but slots change, #flexible when the length changes.
Series capability transitions
Native series have three capability levels:
| State | Read | Write slots | Grow / shrink |
|---|---|---|---|
| Immutable (default) | yes | no | no |
Mutable (#mutable) | yes | yes | no |
Flexible (#flexible) | yes | yes | yes |
Transition at runtime with freeze (immutable), thaw (mutable), flex (growable) and compact (shrink capacity to the current length). Each returns a value with the new capability. thaw and flex are free, in-place unlocks of a series you own: a string literal or a :name borrow cannot be unlocked and is rejected at compile time with B3006. copy mints a fresh owned, writable duplicate, so flex copy s grows a literal or shared series.
fl: make [#flexible [string! 32]] none ; growable start
append fl "hello"
im: freeze fl ; immutable view (plain string!)
mt: thaw im ; mutable view (slot writes)
grow: flex mt ; growable view
append grow " world"
frozen: freeze grow ; back to immutable for safe sharing
print frozen
freeze infers its type only from a #flexible source, so start from a growable series to exercise the whole chain. Growing a non-growable target is T4015.Slices & views
slice! is a borrowed view; it does not own the source storage.
- A view is only valid while the owner storage remains valid; resizing the source can invalidate it.
- Storing call-scoped host views in aggregates or long-lived state is unsafe. Copy into owned Recoil storage when a view must outlive its source call.
- Heterogeneous
block!views are conservative: treat block-origin slices as read-only (B3005) and avoid structural mutation while one is live. - Rebol native extension host views are call-scoped and must not be stored in structs, maps, blocks, module globals, closures or spawned tasks.
Ownership diagnostics
| Code | Meaning |
|---|---|
B3000 | use after ownership transfer |
B3003 | write through a non-mutable binding |
B3004 | borrowed value escapes (including dictionary entries), or a live-view hazard |
B3005 | write through a read-only block-origin slice |
B3006 | thaw/flex of a static string literal or a borrowed reference; use copy |
B3011 | a stack vector escapes its function-local lifetime |
T4015 | grow operation on a non-growable series target |
T4017 | shared managed-native value passed to a #unique parameter |
P1022 | removed mut token used in source |
P1725 | mutable used anywhere except name: mutable value |
See the Reference for the diagnostic message format.