Fiber Architecture

Fibers are Elle's unified control-flow mechanism.

The Fiber

pub struct Fiber {
    pub stack: SmallVec<[Value# 256]>,       // operand stack
    pub frames: Vec<Frame>,                   // call frames (closure + ip + base)
    pub status: FiberStatus,                  // New, Alive, Paused, Dead, Error
    pub mask: SignalBits,                     // which signals parent catches from this fiber
    pub parent: Option<WeakFiberHandle>,      // weak back-pointer (avoids Rc cycles)
    pub parent_value: Option<Value>,          // cached Value for parent
    pub child: Option<FiberHandle>,           // most recently resumed child
    pub child_value: Option<Value>,           // cached Value for child
    pub closure: Rc<Closure>,                 // the closure this fiber wraps
    pub param_frames: Vec<Vec<(u32, Value)>>, // dynamic parameter bindings
    pub signal: Option<(SignalBits, Value)>,  // signal payload or return value
    pub suspended: Option<Vec<SuspendedFrame>>, // frames for resumption
    pub call_depth: usize,                    // stack overflow detection
    pub call_stack: Vec<CallFrame>,           // for stack traces
    pub fuel: Option<u32>,                    // instruction budget (None = unlimited)
    pub withheld: SignalBits,                 // denied capabilities (see capabilities.md)
}

The parent_value and child_value fields cache the Value wrapping the handle, so fiber/parent and fiber/child return identity-preserving values without re-allocating heap objects.

FiberHandle

FiberHandle wraps Rc<RefCell<Option<Fiber>>>. The Option makes "fiber is currently executing on the VM" representable as None.

WeakFiberHandle wraps Weak<RefCell<Option<Fiber>>> for parent back-pointers, avoiding Rc cycles.

FiberStatus

StatusMeaning
NewCreated but never resumed
AliveCurrently executing on the VM
PausedWaiting for resume (signaled or yielded)
DeadCompleted normally
ErrorTerminated by unhandled error

Signals

Signal types are bit positions in a u32 bitmask. User-facing signals use keywords in set literals for fiber masks:

KeywordBitPurpose
:error0Error propagation
:yield1Cooperative suspension
:debug2Breakpoint / trace
:ffi4Calls foreign code
:halt8Graceful VM termination
:io9I/O request to scheduler
:exec11Subprocess completion
:fuel12Instruction budget exhaustion

Bits 3, 5–7, 10, 13–15 are VM-internal. Bits 16–31 are for user-defined signals (via (signal :keyword)).

The terminal signal (bit 10) is uncatchable — it passes through all mask checks. fiber/cancel uses this to ensure the fiber dies immediately.

Signal emission

When code emits a signal (emit):

1. Signal value stored on the fiber 2. Fiber suspends 3. Parent checks: does the mask include this signal? - Caught: parent handles the signal - Not caught: parent also suspends, signal propagates up the chain

Signal mask

The mask on a fiber determines which of its signals the parent catches. Set at creation time, immutable after. The caller decides what to handle, not the callee.

(defn my-fn [] 42)

# Create a fiber that catches errors from its closure
(fiber/new my-fn |:error|)

# Create a fiber that catches yields
(fiber/new my-fn |:yield|)

# Create a fiber that catches both
(fiber/new my-fn |:error :yield|)

Suspension and Resumption

SuspendedFrame

pub enum SuspendedFrame {
    Bytecode(BytecodeFrame),
    FiberResume { handle: FiberHandle, fiber_value: Value },
}

pub struct BytecodeFrame {
    pub bytecode: Rc<Vec<u8>>,
    pub constants: Rc<Vec<Value>>,
    pub env: Rc<Vec<Value>>,
    pub ip: usize,
    pub stack: Vec<Value>,
    pub location_map: Rc<LocationMap>,
    pub push_resume_value: bool,
}

SuspendedFrame is an enum: Bytecode captures everything needed to resume bytecode execution; FiberResume resumes a sub-fiber (used by defer/protect when a sub-fiber's I/O signal propagates through its parent). push_resume_value controls whether the resume value is pushed onto the stack before continuing.

Two suspension modes

Signal suspension (emit): single SuspendedFrame with empty stack. The fiber's own operand stack is preserved in place.

Yield suspension (yield instruction): chain of SuspendedFrames from the yielder to the coroutine boundary. Each frame captures its operand stack. When yield propagates through Call instructions, each caller's frame is appended to the chain.

Frame ordering

Innermost (yielder/signaler) at index 0, outermost (caller) at last index. On resume, frames are replayed forward: index 0 first, last index last.

resume_suspended

VM::resume_suspended(frames, resume_value) -> SignalBits replays the frame chain:

1. For each frame: restore its stack, push the value from the previous frame (or the resume value for the innermost), execute from the saved IP 2. On SIG_OK: extract the result, pass it to the next frame 3. On non-OK signal: save context for potential future resume, merge remaining outer frames for yield signals, return the signal bits

Resume value destination

When a suspended fiber is resumed with a value, the value is pushed onto the fiber's operand stack. The IP points to the instruction after the signal, so execution continues as if the signal expression evaluated to the resume value.

When fiber/resume returns to the parent, the child's signal value is pushed onto the parent's operand stack. Use fiber/status to check whether the child completed normally, errored, or suspended. Use fiber/value to read the signal payload.

Rc Threading

Bytecode and constants flow through the dispatch loop as &Rc<Vec<u8>> and &Rc<Vec<Value>>. This eliminates data copying:

tail calls clone the Rc (cheap), not the Vec

they don't need the Rc

The inner dispatch loop returns (SignalBits, usize) — signal bits and the IP at exit. This eliminates the former suspended_ip staging field.

The VM

pub struct VM {
    pub fiber: Fiber,                          // currently executing fiber (owned)
    pub current_fiber_handle: Option<FiberHandle>, // handle if from fiber/new
    pub docs: HashMap<String, Doc>,             // builtin documentation (shared)
    pub ffi: FFISubsystem,                     // FFI subsystem (shared)
    pub modules: HashMap<String, HashMap<u32, Value>>,
    pub jit_cache: HashMap<*const u8, Rc<JitCode>>,
    pub pending_tail_call: Option<TailCallInfo>, // transient, never crosses suspension
    // ... other shared state
}

The VM owns the currently executing fiber directly — no Rc/RefCell overhead in the hot path. Suspended fibers are wrapped in FiberHandle when stored as values. On resume, the child is swapped in via FiberHandle::take()# on suspend, swapped out via FiberHandle::put().

Parent/Child Chain

Fibers form a chain via parent (weak) and child (strong) pointers.

The child field tracks the most recently resumed child, not all children. It's set on resume and cleared on completion or when a different child is resumed.

Signal propagation

When a child signals and the parent's mask doesn't catch it, the parent also suspends. The entire chain from signaler to eventual handler freezes. Each fiber in the chain is suspended and inspectable.

Walk fiber/child to find the originating fiber. The originator's fiber/value has the payload# intermediaries store (bits, NIL).

Signal System Integration

Closures carry a Signal with signal bits describing what they might emit. The fiber's mask determines which signals are caught. The signal system is compile-time; runtime signals are runtime events. Same bitfield, different timing.

See docs/signals.md for the signal system design.


See also