Zith uses Node Resource Analysis (NRA) to guarantee memory safety without a borrow checker or garbage collector. NRA is the final compiler pass — after SEMA and SOLVE have fully resolved all types — and is responsible for tracking ownership, aliasing, and lifetime validity through the entire program.
ℹ What NRA does: tracks whether a node is alive, dead, or lent; ensures unique nodes have exactly one owner; validates that belong nodes do not outlive their parent; and enforces argument exclusivity in every call expression.
| Modifier | Owns? | Exclusive? | Mutable? | Common Use |
|---|---|---|---|---|
default |
Yes | Yes | Follows binding | Variables, struct fields — no explicit keyword needed |
lend |
No | Yes | Yes | Exclusive mutable temporary borrow for function arguments |
view |
No | No | No | Read-only non-owning reference; many can coexist |
unique |
Yes | Yes | Yes | Single-owner guarantee; ownership transfer patterns |
share |
Yes | No | Yes | Compile-time-proven shared mutable state; no ref-count |
belong |
No | — | Follows parent | Part-of relationship; back-pointers, hierarchies |
💡 In practice, most code only needs lend and view. The rest are for specific ownership patterns.
When no keyword is specified, the type uses default ownership: the binding owns its value exclusively, and the value follows the binding's lifetime.
let pt = Point { x: 1.0, y: 2.0 }; // default ownership
var data = Buffer.new(1024); // default ownership, mutable binding
Moving a value from a to b redirects the name b to a's node. The name a becomes dead and cannot be read — only reassigned:
var a = Point { x: 1.0, y: 2.0 };
let b = a; // b -> a's node; a becomes dead
// @println(a.x); -- COMPILE ERROR: a is dead
@println(b.x); // OK
a = Point { x: 3.0, y: 4.0 }; // OK: reassignment creates a new node for a
Effectively, if a is never reassigned, it is as if a never existed and b has always held the value.
In any call expression, each argument must refer to a distinct node. No exceptions.
default / unique / lend duplicated → ownership errorshare / view duplicated → logic error (same resource in two argument positions is almost certainly a bug)fn update(a: lend Point, b: lend Point) { }
let pt = Point { x: 1.0, y: 2.0 };
// update(pt, pt); -- COMPILE ERROR: same node in two arguments
A symbol may not be read if its node is in state dead.
let a = Point { x: 1.0, y: 2.0 };
let b = a;
// @println(a.x); -- COMPILE ERROR: a is dead after move
belongA belong node may not be stored in any location whose lifetime exceeds any node in its dependency nodes. At every use, all parents must be alive.
struct Node<T> {
data: T,
parent: ?belong Self, // back-pointer; lifetime tied to parent
}
lend Behavioural PromiseA lend value may not be stored, moved, or captured. It may be passed as a call argument or returned (passing the promise to the caller).
fn scale(p: lend Point, factor: f32) { p.x *= factor; p.y *= factor; }
var pt = Point { x: 3.0, y: 4.0 };
scale(pt, 2.0); // pt is lent to scale
@println(pt.x); // OK: lend has ended
lend — Exclusive Temporary BorrowA lend parameter gets exclusive mutable access. The caller retains ownership and regains access after the function returns. belong fields can be passed as lend to functions.
fn move_by(p: lend Point, dx: f32, dy: f32) {
p.x += dx;
p.y += dy;
}
var pt = Point { x: 0.0, y: 0.0 };
move_by(pt, 5.0, 3.0);
// pt is accessible again here — lend has ended
view — Read-Only Non-Owning ReferenceMany view references may coexist. The underlying value cannot be mutated through a view.
fn length(p: view Point): f32 {
sqrt(p.x * p.x + p.y * p.y) // OK: read-only
}
let pt = Point { x: 3.0, y: 4.0 };
let v1: view Point = pt;
let v2: view Point = pt; // fine: multiple views
unique — Single-Owner GuaranteeOnly one name in the graph may point to a unique node. This is ideal for ownership-transfer patterns.
let resource = unique Buffer.new(1024);
let other = resource; // transfer: other now owns
// resource.use(); -- COMPILE ERROR: resource is dead
share — Compile-Time-Proven Sharingshare allows multiple names to point to the same node. All can mutate. No reference-counting — the compiler statically proves correctness.
let a: share Config = loadConfig();
let b: share Config = a; // both point to the same node
b.port = 9090; // mutation visible through a
belong — Part-Of RelationshipA belong node is structurally part of its parent and cannot outlive it. It cannot be stored independently and can be passed as lend to functions. Used for back-pointers and hierarchies.
struct Node<T> {
data: T,
children: []unique Self,
parent: ?belong Self, // back-pointer: lifetime tied to parent
}
fn getParent(self: view Node): lend Node { self.parent }
implement Node<T> {
fn append(self: lend Self, data: T) {
self.children ~= unique Node {
data,
children: [],
parent: belong self, // belong ties lifetime to self
};
}
}
Zith supports self-referential types through unique (owning forward pointers) and belong (non-owning back-pointers). NRA guarantees belong never outlives its owner, eliminating the need for weak pointers or unsafe.
struct Node<T> {
data: T,
next: ?unique Self, // owns next; null at tail
prev: ?belong Self, // back-ref; null at head
}
implement Node<T> {
fn isHead(self): bool { self.prev is null }
fn isTail(self): bool { self.next is null }
fn append(self: lend Self, data: T) {
let new = unique Node { data, next: null, prev: belong self };
self.next = new;
}
}
// Freeing the head frees the entire chain (unique ownership chain)
Inside conditional branches (if/else/when), each branch operates in isolation:
a move, mutation, or invalidation inside one branch does not affect other branches.
After all branches complete, NRA collects all side-effects and applies them to the enclosing scope.
If a conditional expression is used to recover a value but not all paths return, the compiler implicitly deduces null for the missing paths. The result type becomes ?T:
let result = if (v is i32) { v } else { }; // ?i32
NRA cannot statically validate all shared/view cycles across threads. Three escape hatches are available:
await — Structured Concurrencylet handle = spawn worker(shared_data);
await handle; // compiler knows thread is done before scope ends
#wontRemain — Promise#wontRemain let _ = spawn quick_task(shared_data); // promise: thread dies before scope ends
Rc — Runtime Ref-Count Fallbacklet shared = Rc.new(HeavyResource.init());
let _ = spawn worker(Rc.clone(shared)); // runtime reference counting
| Modifier | Owns | Exclusive | Mutable | Can Be Stored | Can Be Returned |
|---|---|---|---|---|---|
default |
Yes | Yes | Follows binding | Yes | Yes |
lend |
No | Yes | Yes | No | Yes (passes promise) |
view |
No | No | No | Yes | Yes |
unique |
Yes | Yes | Yes | Yes | Yes |
share |
Yes | No | Yes | Yes | Yes |
belong |
No | — | Follows parent | Only inside parent | As lend |
default — Start with implicit ownership. Most code doesn't need explicit keywords.view for reading — For functions that only need read access. Allows multiple callers.lend for temporary mutation — When you need exclusive write access for a function call.unique for heap-allocated single-owner data — Moves instead of copies, no aliasing.share intentionally — Only when multiple mutable references are truly needed.belong for back-pointers — Trees, linked lists, graphs where child refers to parent.See also: Spec: Memory Model (NRA)
Next: Memory Management — allocation, scenes, and runtime memory strategies.