Regional Mutability
Regional mutability is Reussir’s construction-phase form of mutation. It is designed for a common two-phase lifetime: first build and connect a data structure locally, then freeze it and use it as immutable data. Mutation stays inside the construction phase; the reading phase uses ordinary managed references.
Reference counting reclaims an object when its count reaches zero. That rule works locally: dropping the final outside reference to an acyclic list reduces the head to zero, destroying it reduces the next node to zero, and the release cascades through the list.
Now connect three nodes into a ring:
┌───────────────┐ ▼ │outside ───▶ A ───▶ B ───▶ C ┘
┌───────────────┐ ▼ │outside ───▶ A ───▶ B ───▶ C ┘
Before outsideoutside is dropped, AA has one incoming edge from CC and one from the outside; BB and CC each have one incoming edge from the preceding node. Dropping outsideoutside changes AA from two references to one. No count is zero, so no destructor starts. The entire ring is unreachable, but every node still keeps another node alive. Plain per-node RC leaks the cycle.
One simple answer is to admit only inductive data without mutation. A constructor can refer to values that already exist, but an older value cannot later be edited to point back to the new value. Construction order is therefore a topological order for the reference graph: a directed cycle cannot be created. For those acyclic values, ordinary RC is sufficient.
That restriction is valuable, but not universal. Compilers, parsers, UI trees, automata, scene graphs, and doubly linked structures can have a natural construction phase in which forward links, back links, or cycles need to be filled after allocation. Reussir keeps the immutable reading model and gives that construction phase a separate boundary.
The Records chapter introduced regional records, [field][field] links, and their flexflex and rigidrigid views. Their lifecycle is:
regional { ... }regional { ... }opens a region.- A
regional fnregional fnallocates into the caller’s active region. Fresh regional records have the[flex][flex]view. - Code in that region may assign through
[field][field]links with->->and:=:=. - A flex value returned as the final expression of
regional { ... }regional { ... }is frozen at the boundary. Outside the block it has the rigid view, normally written simply asNodeNoderather than[rigid] Node[rigid] Node. - Rigid values are immutable and may be retained, shared, and released like other RC-managed values.
Start with a regional node. A [field] Node[field] Node is stored as Nullable<Node>Nullable<Node>, so a new link can be empty:
struct [regional] Node { value: u64, next: [field] Node}regional fn new_node(value: u64) -> [flex] Node { Node { value, next: Nullable::Null {} }}
struct [regional] Node { value: u64, next: [field] Node}regional fn new_node(value: u64) -> [flex] Node { Node { value, next: Nullable::Null {} }}
regional fnregional fn does not open a region by itself. It receives the current region implicitly and returns a flex value that is still local to that region. That lets helpers cooperate during construction:
regional fn make_ring() -> [flex] Node { let a = new_node(1); let b = new_node(2); let c = new_node(3); a->next := Nullable::NonNull { b }; b->next := Nullable::NonNull { c }; c->next := Nullable::NonNull { a }; a}
regional fn make_ring() -> [flex] Node { let a = new_node(1); let b = new_node(2); let c = new_node(3); a->next := Nullable::NonNull { b }; b->next := Nullable::NonNull { c }; c->next := Nullable::NonNull { a }; a}
The three assignments close the cycle while all nodes are flex. A plain function creates the region and lets the root cross its boundary:
fn frozen_ring() -> Node { regional { make_ring() }}
fn frozen_ring() -> Node { regional { make_ring() }}
Inside the block, make_ring()make_ring() has type [flex] Node[flex] Node. At the closing brace, the boundary freezes the escaping graph and the expression as a whole has type NodeNode, the rigid view. Reading the frozen links uses ordinary projection and pattern matching; assignment is no longer available:
fn next_value(node: Node) -> u64 { match node.next { Nullable::NonNull(next) => next.value, Nullable::Null => node.value }}
fn next_value(node: Node) -> u64 { match node.next { Nullable::NonNull(next) => next.value, Nullable::Null => node.value }}
Freezing scans the escaping graph through its [field][field] links and identifies its strongly connected components (SCCs). Each SCC becomes one ownership unit. The three-node ring above is one SCC with one external count, rather than three objects whose internal edges keep three separate counts nonzero. An acyclic node is simply an SCC of size one.
After the scan, reachable components are rigid and use RC outside the region. When the last outside reference to a component is released, the runtime can reclaim the whole component and release its outgoing links to other components. Allocations that were built in the region but are not reachable from the escaping result are reclaimed when the region closes.
The compiler must be able to prove that every live flex object belongs to the current region and becomes either frozen or unreachable when that region ends. The following restrictions maintain that invariant.
Because a regional function needs the caller’s active region, this call has no place to allocate:
struct [regional] Node { value: u64 }regional fn new_node(value: u64) -> [flex] Node { Node { value }}fn outside() -> Node { new_node(1)}
struct [regional] Node { value: u64 }regional fn new_node(value: u64) -> [flex] Node { Node { value }}fn outside() -> Node { new_node(1)}
The compiler reports both the missing region and the attempted flex-to-rigid escape:
Error: cannot call a regional function outside of a region ╭─[ outside-call.rr:8:5 ] │ 8 │ new_node(1) │ ─────┬───── │ ╰─────── cannot call a regional function outside of a region───╯Error: flexivity mismatch: a `Flex` regional value cannot be used where `Rigid` is required (one does not refine to the other) ╭─[ outside-call.rr:8:5 ] │ 8 │ new_node(1) │ ─────┬────────╯
Error: cannot call a regional function outside of a region ╭─[ outside-call.rr:8:5 ] │ 8 │ new_node(1) │ ─────┬───── │ ╰─────── cannot call a regional function outside of a region───╯Error: flexivity mismatch: a `Flex` regional value cannot be used where `Rigid` is required (one does not refine to the other) ╭─[ outside-call.rr:8:5 ] │ 8 │ new_node(1) │ ─────┬────────╯
Wrap the call in regional { ... }regional { ... } to provide the region and freeze its final result, as frozen_ringfrozen_ring does above.
A plain function can be called when no region exists. It therefore cannot take or return a flex value:
struct [regional] Node { value: u64 }fn inspect(node: [flex] Node) -> u64 { node.value}
struct [regional] Node { value: u64 }fn inspect(node: [flex] Node) -> u64 { node.value}
Error: a non-regional function cannot take flex parameter `node`: a flex value cannot escape its region ╭─[ flex-parameter.rr:3:1 ] │ 3 │ ╭─▶ fn inspect(node: [flex] Node) -> u64 { ┆ ┆ 5 │ ├─▶ } │ ╰─────── a non-regional function cannot take flex parameter `node`: a flex value cannot escape its region───╯
Error: a non-regional function cannot take flex parameter `node`: a flex value cannot escape its region ╭─[ flex-parameter.rr:3:1 ] │ 3 │ ╭─▶ fn inspect(node: [flex] Node) -> u64 { ┆ ┆ 5 │ ├─▶ } │ ╰─────── a non-regional function cannot take flex parameter `node`: a flex value cannot escape its region───╯
Make the helper regional fnregional fn if it participates in the same construction phase. If it must be an ordinary function, freeze before calling it and accept a rigid NodeNode instead.
A closure is a heap-managed value that may outlive the call that created it. Capturing a flex reference would materialize that region-local reference in the closure’s environment:
struct [regional] Node { value: u64 }regional fn inspect_later(node: [flex] Node) -> u64 { let inspect = || node.value; inspect()}
struct [regional] Node { value: u64 }regional fn inspect_later(node: [flex] Node) -> u64 { let inspect = || node.value; inspect()}
Error: closure cannot capture `node`: a flex value cannot escape its region ╭─[ closure-capture.rr:4:19 ] │ 4 │ let inspect = || node.value; │ ──────┬────── │ ╰──────── closure cannot capture `node`: a flex value cannot escape its region───╯
Error: closure cannot capture `node`: a flex value cannot escape its region ╭─[ closure-capture.rr:4:19 ] │ 4 │ let inspect = || node.value; │ ──────┬────── │ ╰──────── closure cannot capture `node`: a flex value cannot escape its region───╯
The same principle prevents a closure from returning a flex value. Perform the closure work on scalars or rigid values, or run it after the region has frozen.
Within a regional record, only [field][field] is a mutable regional link. A plain member that names another regional record stores an already-frozen, rigid value. Putting a freshly allocated flex value there would hide a region-local reference inside a materialized member:
struct [regional] Inner { value: u64 }struct [regional] Outer { inner: Inner }regional fn wrap() -> [flex] Outer { let inner = Inner { value: 1 }; Outer { inner }}
struct [regional] Inner { value: u64 }struct [regional] Outer { inner: Inner }regional fn wrap() -> [flex] Outer { let inner = Inner { value: 1 }; Outer { inner }}
Error: flexivity mismatch: a `Flex` regional value cannot be used where `Rigid` is required (one does not refine to the other) ╭─[ plain-member.rr:6:13 ] │ 6 │ Outer { inner } │ ──┬── │ ╰──── flexivity mismatch: a `Flex` regional value cannot be used where `Rigid` is required (one does not refine to the other)───╯
Error: flexivity mismatch: a `Flex` regional value cannot be used where `Rigid` is required (one does not refine to the other) ╭─[ plain-member.rr:6:13 ] │ 6 │ Outer { inner } │ ──┬── │ ╰──── flexivity mismatch: a `Flex` regional value cannot be used where `Rigid` is required (one does not refine to the other)───╯
Use [field] Inner[field] Inner when innerinner is a link within the same construction region. Keep the member plain when it should hold a rigid value produced by a completed, separate region.
Ordinary payload members are fixed when their record is constructed. The ->-> assignment form is reserved for [field][field] links:
struct [regional] Cell { value: u64 }regional fn overwrite(cell: [flex] Cell) { cell->value := 1;}
struct [regional] Cell { value: u64 }regional fn overwrite(cell: [flex] Cell) { cell->value := 1;}
Error: cannot assign to an immutable field ╭─[ immutable-member.rr:4:5 ] │ 4 │ cell->value := 1; │ ────────┬─────── │ ╰───────── cannot assign to an immutable field───╯
Error: cannot assign to an immutable field ╭─[ immutable-member.rr:4:5 ] │ 4 │ cell->value := 1; │ ────────┬─────── │ ╰───────── cannot assign to an immutable field───╯
Declare a link as [field] Node[field] Node and assign a Nullable<Node>Nullable<Node> when the link needs to change. Scalar payload such as valuevalue is chosen at construction.
There is only one lexically active construction region. Opening another one inside it is rejected:
struct [regional] Node { value: u64 }fn nested() -> Node { regional { regional { Node { value: 1 } } }}
struct [regional] Node { value: u64 }fn nested() -> Node { regional { regional { Node { value: 1 } } }}
Error: cannot create a nested region ╭─[ nested-region.rr:5:9 ] │ 5 │ regional { Node { value: 1 } } │ ───────────────┬────────────── │ ╰──────────────── cannot create a nested region───╯
Error: cannot create a nested region ╭─[ nested-region.rr:5:9 ] │ 5 │ regional { Node { value: 1 } } │ ───────────────┬────────────── │ ╰──────────────── cannot create a nested region───╯
A non-regional call frame can act as a region barrier. The called function may open and close its own region because its signature cannot carry flex values from the caller. Its result is rigid before control returns:
struct [regional] Inner { value: u64 }struct [regional] Outer { inner: Inner }fn build_inner() -> Inner { regional { Inner { value: 7 } }}regional fn wrap(inner: Inner) -> [flex] Outer { Outer { inner }}fn build_outer() -> Outer { regional { let inner = build_inner(); wrap(inner) }}
struct [regional] Inner { value: u64 }struct [regional] Outer { inner: Inner }fn build_inner() -> Inner { regional { Inner { value: 7 } }}regional fn wrap(inner: Inner) -> [flex] Outer { Outer { inner }}fn build_outer() -> Outer { regional { let inner = build_inner(); wrap(inner) }}
At runtime, build_innerbuild_inner creates a separate region while build_outerbuild_outer’s region is active. At the language boundary, however, the call is clean: build_innerbuild_inner receives no outer flex object and returns a rigid InnerInner. That barrier prevents flex objects from flowing between the two regions.
A generic definition does not yet know whether TT will be a shared, value, or regional record. Reussir therefore records a regional requirement when TT appears as [flex] T[flex] T or as the element of [field] T[field] T. The abstract definition can elaborate successfully; each concrete specialization is checked during monomorphization.
This example deliberately instantiates both regional positions with an explicitly shared record:
struct [shared] SharedData { value: u64 }regional fn needs_flex<T>(value: [flex] T) -> u64 { 0}regional fn bad_call(value: SharedData) -> u64 { needs_flex(value)}struct [regional] Link<T> { next: [field] T}regional fn bad_link(link: [flex] Link<SharedData>) -> u64 { 0}
struct [shared] SharedData { value: u64 }regional fn needs_flex<T>(value: [flex] T) -> u64 { 0}regional fn bad_call(value: SharedData) -> u64 { needs_flex(value)}struct [regional] Link<T> { next: [field] T}regional fn bad_link(link: [flex] Link<SharedData>) -> u64 { 0}
The generic syntax is valid, but the SharedDataSharedData specializations are not. The monomorphizer reports the requirement at the generic definition that created it:
Error: a `[flex]` type parameter requires a regional record, but this instantiation supplied a non-regional (value/shared) type ╭─[ generic-regional.rr:3:1 ] │ 3 │ ╭─▶ regional fn needs_flex<T>(value: [flex] T) -> u64 { ┆ ┆ 5 │ ├─▶ } │ ╰─────── a `[flex]` type parameter requires a regional record───╯Error: a `[field]` link requires a regional record, but this record was instantiated at a non-regional (value/shared) type ╭─[ generic-regional.rr:11:1 ] │ 11 │ ╭─▶ struct [regional] Link<T> { ┆ ┆ 13 │ ├─▶ } │ ╰─────── a `[field]` link requires a regional record────╯
Error: a `[flex]` type parameter requires a regional record, but this instantiation supplied a non-regional (value/shared) type ╭─[ generic-regional.rr:3:1 ] │ 3 │ ╭─▶ regional fn needs_flex<T>(value: [flex] T) -> u64 { ┆ ┆ 5 │ ├─▶ } │ ╰─────── a `[flex]` type parameter requires a regional record───╯Error: a `[field]` link requires a regional record, but this record was instantiated at a non-regional (value/shared) type ╭─[ generic-regional.rr:11:1 ] │ 11 │ ╭─▶ struct [regional] Link<T> { ┆ ┆ 13 │ ├─▶ } │ ╰─────── a `[field]` link requires a regional record────╯
Instantiate these parameters with a [regional][regional] record instead. A [shared][shared] record is already managed by the ordinary shared RC regime; it cannot be reinterpreted as a flex allocation or stored in a link that the regional scanner expects to traverse.