Memory Model
How Resilient represents, owns, and reclaims values across its three execution tiers.
Table of contents
Scope
Resilient is aimed at safety-critical embedded workloads (automotive, aerospace, medical). Memory behaviour therefore has to be auditable end-to-end: a reviewer needs to answer “where does this value live, who owns it, when is it freed, and can this program allocate under this feature config?” without tracing through the implementation.
This page documents all three execution tiers and the shared properties that hold across them.
Overview: three tiers
Resilient programs can execute on three backends. They share the same language-level value semantics but differ in their implementation memory strategy.
| Tier | Implementation | Allocation strategy |
|---|---|---|
| Host interpreter | resilient/src/main.rs — tree walker |
Rc<RefCell<EnvFrame>> (refcount + interior mutability) |
| Bytecode VM | resilient/src/vm.rs — stack machine |
Owned Vec<Value> operand stack + locals slab |
| Cranelift JIT | resilient/src/jit_backend.rs |
Cranelift-managed native stack frames + closure upvalues |
| Embedded runtime | resilient-runtime/ (#![no_std]) |
Stack-only by default; optional alloc feature |
The host tier optimises for iteration speed and closure semantics. The VM tier keeps allocation out of the inner dispatch loop. The embedded tier is the one safety auditors usually care about — it is the configuration shipped onto hardware.
Value representation
Host Value enum
Defined at resilient/src/main.rs:4000. The variants are:
| Variant | Payload | Backing storage |
|---|---|---|
Int(i64) |
64-bit signed integer | Inline (stack / enum payload) |
Float(f64) |
IEEE-754 double | Inline |
Bool(bool) |
1 bit logical | Inline |
String |
std::string::String |
Heap (owned Vec<u8> in the String) |
Array |
Vec<Value> |
Heap |
Struct |
Vec<(String, Value)> |
Heap (field list) |
Map |
HashMap<MapKey, Value> |
Heap |
Set |
HashSet<MapKey> |
Heap |
Bytes |
Vec<u8> |
Heap |
Result |
{ ok: bool, payload: Box<Value> } |
Heap (the payload box) |
Return |
Box<Value> (internal control-flow carrier) |
Heap |
Function |
parameters + Box<Node> body + Environment |
Heap (AST body) + Rc (env) |
Builtin |
&'static str name + native fn pointer |
Inline |
Closure |
{ fn_idx: u16, upvalues: Box<[Value]> } (VM) |
Heap (upvalue slab) |
Void |
unit | Inline |
Arithmetic semantics match across every tier: Int uses wrapping
i64 ops; Float is IEEE-754; mixed-type ops are a TypeMismatch
error, never an implicit coercion.
Embedded runtime Value enum
The sibling crate at resilient-runtime/src/lib.rs deliberately
carries a narrower surface. Under the default feature set, it
contains only variants whose payload fits on the stack:
Int(i64)Bool(bool)Float(f64)
With --features alloc enabled, it additionally exposes:
String(alloc::string::String)— only variant that pullsalloc.
The enums are deliberately non-unified today. The host Value
transitively pulls in std (closures carry Box<Node>, maps use
HashMap); the embedded enum is kept small so users can audit
exactly what memory a deployed binary can touch.
Ownership and lifetimes
Resilient is a value-semantics language at the source level. Users do not write references, borrows, or lifetimes — those are implementation details of the host runtime, not part of the language.
Bindings
let x = expr;evaluatesexpr, producing aValue, and binds it to the namexin the current environment frame. The value is owned by that binding.letintroduces a new binding; rebindingxin the same scope withletshadows the old one. Mutation of the original slot is a separate operation (assignment via=), not rebinding.- Function parameters are bound to the argument values on entry. They are locals of the callee’s frame and die when the frame returns.
No language-level aliasing
There is no way to spell a reference to another binding. Passing
a value into a function gives the callee a fresh Value. In the
host tier the underlying Rc may be shared (clone is a refcount
bump), but the user cannot observe this — mutation of heap-backed
values (Array, Map, Struct) is done through the binding that
owns it, and there is no syntax for “take a reference to x”.
This matters for auditors: two distinct bindings cannot be proven to alias at the source level, so reasoning about “did this function modify that array?” reduces to “did this function receive that array as an argument or as a captured closure upvalue?”
Reclamation
- Host:
Rc<RefCell<EnvFrame>>is dropped when the last reference goes away. Rust’sDroprecursively frees the value graph. No tracing GC, no stop-the-world pause. - VM: Values are owned in
Vec<Value>slots (operand stack, locals slab). They are freed when their slot is overwritten or the frame is popped. - Embedded: Stack-only variants need no reclamation. With
--features alloc,Stringdrops through the user-installed allocator when the owning binding is freed.
There is no cycle collector. Cycles in the host tier would require a language-level reference type, which Resilient does not expose.
The live-block memory contract
A live { } block is the language’s recoverable-failure primitive.
The host implementation is at resilient/src/main.rs:6679
(eval_live_block).
What is snapshotted
On entry to a live { } block, the interpreter calls
self.env.deep_clone(). That routine:
- Allocates a fresh
RefCell<EnvFrame>for every frame in the scope chain up to the root. - Copies each frame’s
HashMap<String, Value>by value.Valueitself isClone, so primitive variants copy inline and heap-backed variants clone their heap payload. - Follows the
outerchain so the entire captured environment is independent of the live state.
The snapshot is stashed for the duration of the block and
reinstated with another deep_clone on every retry so the first
retry’s mutations do not pollute the second.
What is restored on retry
On a recoverable error or invariant violation, the interpreter:
- Increments the retry counter.
- Optionally sleeps per the configured backoff
(
with backoff(base: ..., factor: ..., max: ...)). - Re-points
self.envat a fresh deep clone of the snapshot. - Re-executes the body.
This means all source-level bindings — including lets introduced
inside the live block and mutations to outer-scope variables — are
rolled back. The block sees exactly the environment it saw on its
first attempt.
What is NOT captured
The snapshot is purely the environment of source-level bindings. It does not and cannot rewind:
- I/O effects.
println, sink writes, file I/O, register writes. These have already left the program’s memory boundary by the time the retry fires. - External system state. Peripheral registers, DMA buffers, sensor readings, network peers.
- Native allocator state. If a
Stringwas allocated during the failing attempt and dropped, its slot in the allocator is already freed; the retry re-allocates. - Process-global counters.
LIVE_TOTAL_RETRIESandLIVE_TOTAL_EXHAUSTIONSare diagnostic counters and intentionally persist across retries.
Retry and timeout budget
Default retry cap is 3 attempts. A within <duration> clause
imposes an additional wall-clock budget sampled once at block
entry; retries and backoff sleeps both count against it. Exceeding
either budget fails the block with a diagnostic that carries the
retry depth. See the eval_live_block implementation for exact
wording and LIVE_RETRY_STACK for how nested blocks compose.
Embedded memory modes
The resilient-runtime crate ships three mutually-consistent
postures. Pick one at build time; they are enforced by feature
flags.
Default: stack-only
cargo build --target thumbv7em-none-eabihf
The default feature set carries only Int, Bool, and Float.
None of these require an allocator — the payload of each enum
variant fits in the value itself. A binary built with default
features has no heap allocation path in the runtime crate.
Explicit heap: --features alloc
cargo build --target thumbv7em-none-eabihf --features alloc
Enables the Value::String variant. Pulls in
alloc::string::String, which requires extern crate alloc and
therefore a #[global_allocator] somewhere in the final binary.
The runtime crate does not pick one — the user is responsible
for wiring it. The Cortex-M4F demo uses
embedded-alloc::LlffHeap with
a static 4 KiB pool; see /no-std
for the canonical pattern.
Assertion posture: --features static-only
cargo build --target thumbv7em-none-eabihf --features static-only
Asserts no-heap intent. The crate emits a compile_error! if
static-only and alloc are both enabled in the same build
graph, catching accidental heap pull-in from transitive
dependencies at link time rather than at runtime. Test coverage
at lib.rs:306+ verifies that the String variant is
exhaustively absent when this flag is active.
User responsibility
The runtime does not enforce a heap size, does not install an OOM handler, and does not allocate eagerly. Binary authors choose:
- Which allocator (
LlffHeap,TlsfHeap, a custom bump, …). - The backing memory (usually a
static mut [MaybeUninit<u8>; N]). - The OOM handler (
#[alloc_error_handler]). - Whether to link
allocat all.
Supported embedded targets
Verified via CI cross-compilation:
| Target | Class |
|---|---|
thumbv7em-none-eabihf |
Cortex-M4F / M7F |
thumbv6m-none-eabi |
Cortex-M0+ |
riscv32imac-unknown-none-elf |
RISC-V 32-bit IMAC |
All three build cleanly under both the default and alloc feature
sets. Cortex-M4F additionally has a runnable demo at
resilient-runtime-cortex-m-demo/.
Atomics
The runtime crate does not use atomics today. Some embedded targets
(notably thumbv6m-none-eabi on Cortex-M0+) lack native
compare-and-swap, and the runtime avoids features that would force
a compiler_builtins shim. Tickets RES-141 (host-side retry
counters) and RES-177 (runtime-level sink sequencing) track where
atomic use has been considered and what the platform story will be.
Comparison to other approaches
vs Rust ownership
Resilient exposes value semantics, not borrow semantics.
The language has no &T, no &mut T, no lifetimes, and no
borrow checker. The host runtime uses Rust’s ownership internally
but the user never names a reference.
Tradeoff: users give up Rust’s zero-copy sharing but gain a simpler mental model — an argument passed to a function is conceptually a copy, period. For safety auditing this is a feature: the set of ways two pieces of code can affect the same value is strictly smaller.
vs manual C malloc/free
There is no malloc, no free, and no pointer type in the
language. The runtime frees values deterministically (scope exit
in the VM; Rc drop in the host), so:
- No
use-after-free— a freed slot is no longer reachable. - No
double-free— Rust’s drop discipline enforces linearity. - No dangling pointers — references do not exist at the source level.
vs tracing GC
The host tier uses reference counting, not a tracing GC. There is no stop-the-world pause, no mark phase, no global heap walk. Drop work is bounded by the size of the value graph going out of scope at that moment.
The embedded tier has no GC at all in the default configuration
(nothing to collect — stack-only values). With --features alloc,
reclamation is still deterministic drop-on-scope-exit through the
user-installed allocator.
Safety properties
Properties that hold across all three tiers unless noted.
- No null. The
Valueenum has no null variant.Voidis a unit value, not a null pointer. An absent result is either aValue::Result { ok: false, ... }or a live-block error. - No use-after-free. Bindings own their values. Once a binding’s scope ends its value is dropped; there is no syntax that yields a handle to a dropped value.
- No dangling pointers. Pointers are not exposed to the
language. The host tier’s
Rcprevents the underlying frame from being freed while any clone exists. - Bounded call depth. The VM enforces
MAX_CALL_DEPTH = 1024frames and returnsVmError::CallStackOverflowpast that. The host interpreter inherits the native Rust stack’s limit (typically 8 MiB) and will overflow with a Rust panic on deeper recursion — this is a known gap and tracked for parity. - Bounded live-block retries. Default cap 3 attempts, plus an
optional
within <duration>wall-clock cap. - Provably heap-free builds. With
--features static-onlyonresilient-runtime, no runtime code path can reach an allocator. Thecompile_error!guard prevents theallocfeature from sneaking in via a transitive dependency. - Deterministic reclamation. Every tier reclaims memory at a
statically predictable point (scope exit,
ReturnFromCall, orRcrefcount reaching zero). No background collector, no unpredictable pause.
Cross-references
/no-std— feature flags, cross-compile targets, allocator wiring walkthrough./philosophy— the resilience / verifiability / simplicity pillars that the memory model is designed to serve./syntax— source-level syntax forlive,let, bindings, and structs.