Resilient Memory Model
Overview
Resilient’s memory model defines how memory is allocated, accessed, and guaranteed to be safe across the compiler, runtime, and embedded targets. This document specifies the allocation tiers, aliasing rules, and mutual exclusivity guarantees that enable safe-critical systems programming.
Allocation Tiers
Memory in Resilient is organized into four distinct tiers, each with different safety guarantees and use cases.
Tier 1: Stack Allocation
Characteristics:
- Automatic allocation on function entry
- Automatic deallocation on function exit
- LIFO (last-in-first-out) ordering
- Bounded size known at compile time
- Zero runtime overhead
Safety Guarantees:
- All accesses are valid within the function scope
- No use-after-free possible
- No memory leaks possible
- Aliasing is permitted but tracked by the type system
Usage:
fn process(int x) -> int {
let local = x + 1; // Stack allocated
return local; // Automatically freed on return
}
Constraints:
- Size must be compile-time constant
- Variable-length arrays require explicit heap allocation
- Recursion is safe but bounded by stack depth
Tier 2: Static Allocation
Characteristics:
- Allocated in the binary’s data section
- Lifetime is the entire program execution
- Accessible from any function
- Zero runtime allocation cost
- Address known at compile time on most targets
Safety Guarantees:
- Always accessible (never deallocated)
- Address never changes
- Safe to store as constant pointers
- Concurrency-safe if declared as immutable
Usage:
static CONFIG = [1, 2, 3, 4, 5];
fn read_config(int index) -> int {
return CONFIG[index]; // Always safe
}
Constraints:
- Size must be compile-time constant
- Initialization must be constant expressions
- Mutable static requires explicit synchronization for concurrency
Tier 3: Heap Allocation
Characteristics:
- Dynamically allocated at runtime
- Size determined at runtime
- Lifetime managed by programmer or runtime GC
- May require deallocation
- Available only with
#[cfg(feature = "alloc")]
Safety Guarantees:
- Bounds checked on every access
- Type information preserved
- Lifetime tracked (when using reference semantics)
- No double-free with proper ownership rules
Usage:
#[cfg(feature = "alloc")]
fn process_array(int count) -> array<int> {
let data = allocate::<int>(count);
for i in 0..count {
data[i] = i * 2;
}
return data; // Ownership transferred
}
Constraints:
- Requires allocator (unavailable in strict
no_std) - Lifetime rules must be followed to prevent use-after-free
- Allocation may fail (returns Option or Result)
Tier 4: MMIO Allocation
Characteristics:
- Memory-mapped I/O registers on embedded systems
- Address fixed by hardware specification
- Accessed via volatile reads/writes
- Lifetime is entire program (hardware register)
- Platform-specific
Safety Guarantees:
- Address is guaranteed by hardware specification
- Access ordering is preserved (volatile semantics)
- Type information ensures correct register widths
- Safe concurrent access with proper synchronization
Usage:
#[mmio(base = "0x40010800", size_bytes = "0x400")]
struct GPIOA {
#[bits(0..=15), rw]
mode: u16,
#[bits(16..=31), ro]
status: u16,
}
Constraints:
- Address must be valid for the target hardware
- Access width must match hardware specification
- Volatile semantics prevent compiler optimizations that would change timing
Aliasing Rules
Resilient follows Rust-like ownership and borrowing rules to prevent data races and use-after-free bugs.
Exclusive Access (Mutable References)
Only one mutable reference to a value may exist at a time:
fn modify(data: &mut array<int>) {
// This is the only way to access `data`
// No other references can exist
for i in 0..data.len() {
data[i] = data[i] + 1;
}
}
Shared Access (Immutable References)
Multiple immutable references may coexist:
fn read_multiple(data: &array<int>) -> int {
// Multiple readers can call this simultaneously
// No writer can exist while readers are active
return data[0] + data[1];
}
No Dangling Pointers
References cannot outlive their referents:
fn safe_borrow(x: int) -> &int {
return &x; // ✗ ERROR: `x` is deallocated on return
}
fn safe_return() -> &static int {
return &STATIC_VALUE; // ✓ OK: static lifetime
}
Mutability Semantics
Immutable by Default
let x = 5; // x is immutable - cannot change
x = 10; // ✗ ERROR
Explicit Mutability
let mut x = 5; // x is mutable - can change
x = 10; // ✓ OK
Interior Mutability for Concurrency
static COUNTER = cell<int>(0);
fn increment() {
COUNTER.set(COUNTER.get() + 1); // Safe mutation of static
}
Guarantees Across Feature Tiers
| Guarantee | std |
no_std |
no_std + alloc |
Embedded MMIO |
|---|---|---|---|---|
| Stack allocation | ✅ | ✅ | ✅ | ✅ |
| Static allocation | ✅ | ✅ | ✅ | ✅ |
| Heap allocation | ✅ | ❌ | ✅ | ❌ |
| MMIO access | ⚠️ | ✅ | ⚠️ | ✅ |
| Concurrency | ✅ | ✅ | ✅ | ✅ |
| GC (garbage collection) | ✅ | ❌ | ❌ | ❌ |
Memory Safety Invariants
These invariants are maintained by the compiler and enforced at runtime:
- No use-after-free: References cannot access deallocated memory
- No double-free: Memory is freed exactly once
- No dangling pointers: References do not outlive their referents
- No data races: Exclusive access prevents simultaneous mutations
- Type safety: All memory accesses respect type constraints
- Bounds safety: All array/slice accesses are in bounds
Embedded Target Specifics
Cortex-M (ARM Embedded)
- Stack: SRAM with size known at link time
- Static: Flash or SRAM (zero-initialized BSS section)
- MMIO: Memory-mapped peripherals at fixed addresses
- Guarantees: Full memory safety with predictable timing
RISC-V
- Stack: Similar to Cortex-M
- Static: Program flash and RAM regions
- MMIO: Device-specific register maps
- Guarantees: Full memory safety, configurable MPU regions
Examples: Safe Patterns
Pattern 1: Stack-Allocated Buffer
fn process_frame(int size) -> int {
let buffer: array<int>(256) = [0..256]; // Stack alloc, bounded
for i in 0..size {
buffer[i] = i * 2;
}
return buffer[0]; // Auto-freed on return
}
Pattern 2: Static Configuration
static DEVICE_CONFIG = {
address: 0x40010800,
timeout: 1000,
flags: 0xABCD,
};
fn get_timeout() -> int {
return DEVICE_CONFIG.timeout;
}
Pattern 3: Mutable Reference
fn zero_array(data: &mut array<int>) {
for i in 0..data.len() {
data[i] = 0;
}
}
fn main() {
let mut arr = [1, 2, 3, 4, 5];
zero_array(&mut arr); // Exclusive borrow
}
Next Steps
Follow-up PRs will:
- Enforce memory tier constraints in the compiler
- Add explicit lifetime annotations for complex references
- Implement runtime bounds checking with zero-cost optimizations
- Define unsafe blocks and preconditions for system programming
References
- RES-3504: Specify and enforce the memory model
- RES-3501: Stabilize the language reference and feature-tier policy
- LANGUAGE.md: Feature tier classification framework