Memory Model

Ori uses Automatic Reference Counting (ARC) for memory management — no garbage collector pauses, no manual memory management, and no borrow checker.

How ARC Works

Every value has a reference count. When you assign a value, the count increases. When a reference goes out of scope, the count decreases. When it hits zero, the memory is freed.

let a = [1, 2, 3];      // ref count = 1
let b = a;              // ref count = 2 (a and b share the data)
// b goes out of scope  // ref count = 1
// a goes out of scope  // ref count = 0, memory freed

Deterministic Cleanup

Unlike garbage collection, ARC frees memory immediately when the last reference is gone:

@process_file (path: str) -> void uses FileSystem = {
    let data = FileSystem.read(path: path);  // Memory allocated
    let result = process(data: data);
    print(msg: result)
}  // data freed exactly here, not "sometime later"

This predictability is valuable for resource-constrained environments and real-time applications.

Why No Garbage Collector?

FeatureGCARC
Pause timesUnpredictableNone
Memory overheadHigherLower
Cleanup timingEventuallyImmediate
PerformanceVariableConsistent

Ori chose ARC for:

  • Predictable performance
  • Lower memory overhead
  • Immediate cleanup
  • Simpler runtime

Preventing Reference Cycles

ARC can’t handle reference cycles. If A references B and B references A, neither can ever be freed. Ori’s design prevents cycles:

1. Sequential Data Flow

Data flows forward through run/try:

{
    let a = create_a();
    let b = create_b(input: a);   // b can reference a
    let c = create_c(input: b);   // c can reference b
    // No way for a to reference c (c doesn't exist when a is created)
}

2. Capture by Value

Closures capture variables by value, not reference:

let x = 10;
let f = () -> x + 1;  // f captures a COPY of x = 10

// Even if we could reassign x, f still has 10
f();  // Always returns 11

This means closures can’t create cycles by capturing “self” references.

3. No Self-Referential Types

You can’t create types that reference themselves through the same instance:

// This pattern is NOT possible in Ori:
type Node = {
    value: int,
    parent: Option<Node>,  // Can't point back to containing instance
}

Instead, use:

  • Indices into collections
  • Separate parent/child structures
  • Tree patterns where children don’t reference parents

Value vs Reference Types

Ori distinguishes between value types (copied) and reference types (reference counted):

Value Types

Copied when assigned:

let x = 42;
let y = x;  // y is a copy, independent of x

// Modifying y doesn't affect x

Value types include:

  • int, float, bool
  • char, byte
  • Duration, Size
  • Small structs (≤32 bytes, containing only primitives)

Reference Types

Shared with reference counting:

let a = [1, 2, 3];
let b = a;  // b and a share the same data

// Both refer to the same underlying list

Reference types include:

  • str
  • [T] (lists)
  • {K: V} (maps)
  • Set<T>
  • Large structs or those containing references

How to Know Which Is Which

General rule:

  • Primitives and small fixed-size types → value
  • Collections and dynamically-sized types → reference

When in doubt, the compiler optimizes appropriately.

The Clone Trait

To get an independent copy of a reference type, use .clone():

let a = [1, 2, 3];
let b = a.clone();  // b has its own copy of the data

// Modifying b doesn't affect a

Clone Is Explicit

Ori requires explicit cloning to avoid hidden performance costs:

// This shares data (cheap)
let shared = expensive_data;

// This copies data (potentially expensive)
let copy = expensive_data.clone();

Clone Trait Definition

trait Clone {
    @clone (self) -> Self
}

What Implements Clone

  • All primitives
  • All collections (when element types implement Clone)
  • Option<T> and Result<T, E> (when inner types implement Clone)
  • Derivable for user types
#derive(Clone)
type Point = { x: int, y: int }

let p1 = Point { x: 10, y: 20 };
let p2 = p1.clone();  // Independent copy

Deep Clone

Cloning is recursive — cloning a container clones its elements:

let lists = [[1, 2], [3, 4]];
let copy = lists.clone();  // Both outer and inner lists are cloned

Closures and Capture

Closures capture variables by value at creation time:

@make_adder (n: int) -> (int) -> int = {
    let add_n = x -> x + n;  // Captures n by value
    add_n
}

let add_5 = make_adder(n: 5);
let add_10 = make_adder(n: 10);

add_5(3);   // 8
add_10(3);  // 13

Snapshot Semantics

The closure sees a snapshot of values at creation:

let x = 10;
let f = () -> x;  // Captures x = 10

// Later changes don't affect f's captured value
let x = 20;  // Shadowing, creates new binding
f();  // Still returns 10 (captured value)

No Outer Mutation

Closures cannot mutate outer scope:

// This won't work as you might expect
let counter = 0;
let increment = () -> {
    counter = counter + 1;  // ERROR: can't mutate outer scope
};

Instead, return the new value or use explicit state:

@make_counter () -> () -> int = {
    let count = { value: 0 };
    () -> {
        count.value = count.value + 1;
        count.value
    }
}

Tail Call Optimization

Ori guarantees tail call optimization (TCO) for recursive functions:

@countdown (n: int) -> void =
    if n <= 0 then () else countdown(n: n - 1)

countdown(n: 1000000);  // No stack overflow

A call is in tail position if it’s the last thing before the function returns.

Tail Position Examples

// Tail call — optimized
@factorial (n: int, acc: int) -> int =
    if n <= 1 then acc else factorial(n: n - 1, acc: n * acc)

// NOT tail call — multiplication happens after the recursive call
@factorial_not_tail (n: int) -> int =
    if n <= 1 then 1 else n * factorial_not_tail(n: n - 1)

Converting to Tail Recursive

Use an accumulator parameter:

// Not tail recursive
@sum (numbers: [int]) -> int =
    if is_empty(collection: numbers) then
        0
    else
        numbers[0] + sum(numbers: numbers.skip(count: 1))

// Tail recursive (with accumulator)
@sum_tail (numbers: [int], acc: int) -> int =
    if is_empty(collection: numbers) then
        acc
    else
        sum_tail(numbers: numbers.skip(count: 1), acc: acc + numbers[0])

ARC Safety Invariants

The Ori language maintains these invariants to ensure ARC safety:

1. No Shared Mutable References

Only one reference can mutate data at a time:

let a = [1, 2, 3];
let b = a;        // Shares data
a[0] = 10;        // Creates new list for a, b still has [1, 2, 3]

2. Closures Capture by Value

No closure can hold a mutable reference to outer scope:

let x = 10;
let f = () -> x;  // Copies x, doesn't reference it

3. No Self-Referential Structures

Types cannot contain references to their own instances:

// Not allowed: Node can't point to itself
type Node = { value: int, next: Option<Node> }

// Allowed: Indices instead of references
type NodeIndex = int
type Graph = { nodes: [Node], edges: [(NodeIndex, NodeIndex)] }

4. Immutable by Default

Module-level bindings must use $:

pub let $CONFIG = { ... };  // Immutable, safe to share

Memory Patterns

Avoid Unnecessary Cloning

// Expensive: clones for each iteration
for item in items.clone() do
    process(item: item)

// Cheap: iterates without cloning
for item in items do
    process(item: item)

Share Immutable Data

// If you don't need to modify, share
let shared = large_data;
use_data(data: shared);
use_data_again(data: shared);

// Only clone when you need independence
let modified = large_data.clone();
modified[0] = new_value;

Use Structural Sharing

Ori collections use structural sharing internally:

let a = [1, 2, 3, 4, 5];
let b = [...a, 6];  // Shares structure with a where possible

Complete Example

// Immutable tree using indices instead of references
type NodeId = int

type TreeNode<T> = {
    id: NodeId,
    value: T,
    children: [NodeId],
}

type Tree<T> = {
    nodes: {NodeId: TreeNode<T>},
    root: Option<NodeId>,
    next_id: NodeId,
}

impl<T> Tree<T> {
    @new () -> Tree<T> =
        Tree { nodes: {}, root: None, next_id: 0 }

    @add_node (self, value: T, parent: Option<NodeId>) -> (Tree<T>, NodeId) = {
        let id = self.next_id;
        let node = TreeNode { id, value, children: [] };

        // Add node to nodes map
        let nodes = { ...self.nodes, id: node };

        // Update parent's children if parent exists
        let nodes = match parent {
            Some(parent_id) -> {
                let parent_node = nodes[parent_id];
                match parent_node {
                    Some(p) -> {
                        ...nodes
                        parent_id: TreeNode { ...p, children: [...p.children, id] }
                    }
                    None -> nodes
                }
            }
            None -> nodes
        };

        // Set root if this is first node
        let root = if is_none(option: self.root) then Some(id) else self.root;

        (Tree { nodes, root, next_id: id + 1 }, id)
    }

    @get_node (self, id: NodeId) -> Option<TreeNode<T>> =
        self.nodes[id]

    @children (self, id: NodeId) -> [TreeNode<T>] = {
        let node = self.nodes[id];
        match node {
            Some(n) -> for child_id in n.children yield match self.nodes[child_id] {
                Some(c) -> c
                None -> continue
            }
            None -> []
        }
    }
}

@test_tree tests _ () -> void = {
    let tree = Tree<str>.new();

    let (tree, root) = tree.add_node(value: "root", parent: None);
    let (tree, child1) = tree.add_node(value: "child1", parent: Some(root));
    let (tree, child2) = tree.add_node(value: "child2", parent: Some(root));

    assert_some(option: tree.get_node(id: root));
    assert_eq(actual: len(collection: tree.children(id: root)), expected: 2)
}

// Demonstrates safe closure capture
@make_processor<T: Clone> (config: Config) -> (T) -> Result<T, Error> = {
    // config is captured by value
    let process = item -> {
        if config.validate then
            validate(item: item)?;
        Ok(transform(item: item, config: config))
    };
    process
}

// Tail recursive processing
@process_list<T> (items: [T], processor: (T) -> T, acc: [T]) -> [T] =
    if is_empty(collection: items) then
        acc
    else
        process_list(
            items: items.iter().skip(count: 1).collect(),
            processor: processor,
            acc: [...acc, processor(items[0])],
        )

@test_process_list tests @process_list () -> void = {
    let items = [1, 2, 3, 4, 5];
    let result = process_list(
        items: items
        processor: x -> x * 2
        acc: []
    );
    assert_eq(actual: result, expected: [2, 4, 6, 8, 10])
}

Quick Reference

Reference Counting

let a = value;         // ref count = 1
let b = a;             // ref count = 2
// b drops            // ref count = 1
// a drops            // ref count = 0, freed

Clone

let copy = original.clone();  // Independent copy

Value vs Reference

Value TypesReference Types
int, float, boolstr, [T], {K: V}
char, byteSet<T>
Small structsLarge structs

Closure Capture

let x = 10;
let f = () -> x;  // Captures x by value (snapshot)

Tail Call

// Tail position — optimized
@fn (n: int, acc: int) -> int =
    if done then acc else fn(n: n - 1, acc: acc + n)

Safety Invariants

  1. No shared mutable references
  2. Closures capture by value
  3. No self-referential structures
  4. Immutable module-level bindings

What’s Next

Now that you understand the memory model: