Lifetimes & zero-copy
This is the lesson that ties the whole data layer together. Back in 2.3 you saw the engine
reads fields by borrowing — resolve returns &Value. But a borrow raises an obvious
danger: what if the record is gone and you still hold the reference? The answer is
lifetimes, and they’re what make Clinker’s zero-copy reads not just fast but safe.
You’ll be able to: read a lifetime annotation, explain how it prevents a dangling
reference, and describe how RecordView reads a field with zero allocation.
A lifetime is “how long this borrow is valid”
Section titled “A lifetime is “how long this borrow is valid””A reference can’t outlive the thing it points at. A lifetime (written 'a) is the
compiler’s name for “the span during which this borrow is valid.” Most of the time you
never write one — the compiler infers them. But when a type holds a borrow, it must say
so, and that annotation is what guarantees the borrow can’t dangle.
The engine’s zero-copy field reader is exactly such a type. RecordView<'a, S> is a tiny,
Copy handle — a pointer plus an index — that borrows into record storage for the
lifetime 'a:
clinker-record ·record_view.rs ·RecordView type @47d2e12
#[derive(Clone, Copy)]pub struct RecordView<'a, S: RecordStorage + ?Sized> { storage: &'a S, // borrows the record storage for the lifetime 'a index: u64, // which record}The 'a ties the view to the storage it came from. The compiler will not let a
RecordView outlive that storage — so a view onto a record that’s been dropped is not a
runtime crash you debug, it’s a program that won’t compile. The storage it reads
through is itself a trait, so the same view works over the real arena in production and a
stub in tests:
clinker-record ·storage.rs ·RecordStorage trait @47d2e12
pub trait RecordStorage: Send + Sync { fn resolve_field(&self, index: u64, name: &str) -> Option<&Value>; // ... resolve_qualified, available_fields, record_count}Zero-copy, and the one clone
Section titled “Zero-copy, and the one clone”Put the pieces together. Reading a field returns &Value (a borrow, 2.3), the absent case
is None (2.4), and shared backing data lives behind Arc (2.7). So evaluating a CXL
expression — coalescing, comparing, filtering — walks through fields without copying a
single one. The string "active" is read in place, compared, and discarded; it’s never
duplicated. A copy is paid exactly once, and only at the spot that must keep a value
past the borrow (an output that writes it, say) — not on every read along the way.
There’s even a small trick for the absent case: rather than allocate an empty value to
return, the resolver can hand back a borrow of a single shared static “null” value — a
reference with a 'static lifetime that’s always valid, so None-ish reads cost nothing
either.
Lifetimes stop a dangling read
Section titled “Lifetimes stop a dangling read”> output appears here — press Run
Run it, then uncomment the two lines. The borrow checker refuses to compile a program
where field could be read after record is gone. That refusal — at compile time, every
time — is what lets the engine borrow fearlessly.
// quick check
What does the lifetime 'a in RecordView<'a, S> guarantee?
The 'a ties the view to its storage. The compiler rejects any code where the view could be used after the storage is gone, so zero-copy reads can never dangle.
Checkpoint — and the end of Phase 2
Section titled “Checkpoint — and the end of Phase 2”That’s Phase 2. You’ve gone from “a record is a row of cells” to the real machinery: a
32-byte closed Value, exhaustive match, borrow-don’t-copy field access, Option/Result
for absence and failure, Arc-shared schemas and context, and lifetimes that make all the
borrowing provably safe. Phase 3 — Planning & Expressions moves up a layer: how the
engine turns YAML and CXL into the validated CompiledPlan you’ve been running.