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.
take()— extract the fiber (sets slot to None)put()— return the fiber (sets slot to Some)with()/with_mut()— borrow in-place for read/write
WeakFiberHandle wraps Weak<RefCell<Option<Fiber>>> for parent back-pointers, avoiding Rc cycles.
FiberStatus
| Status | Meaning |
|---|---|
New | Created but never resumed |
Alive | Currently executing on the VM |
Paused | Waiting for resume (signaled or yielded) |
Dead | Completed normally |
Error | Terminated by unhandled error |
Signals
Signal types are bit positions in a u32 bitmask. User-facing signals use keywords in set literals for fiber masks:
| Keyword | Bit | Purpose |
|---|---|---|
:error | 0 | Error propagation |
:yield | 1 | Cooperative suspension |
:debug | 2 | Breakpoint / trace |
:ffi | 4 | Calls foreign code |
:halt | 8 | Graceful VM termination |
:io | 9 | I/O request to scheduler |
:exec | 11 | Subprocess completion |
:fuel | 12 | Instruction 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:
execute_bytecodewraps raw slices inRconce at the public boundaryexecute_bytecode_from_ip/execute_bytecode_saving_stacktake&RcTailCallInfois(Rc<Vec<u8>>, Rc<Vec<Value>>, Rc<Vec<Value>>)—
tail calls clone the Rc (cheap), not the Vec
handle_emit/handle_callclone the Rc intoSuspendedFrame- Individual instruction handlers dereference to
&[u8]/&[Value]—
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.
parent.child = child_handleis set before executing the child- On signal caught (SIG_OK or mask match):
parent.child = None - On signal NOT caught (propagates):
parent.childstays set (trace chain)
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
- Signal index
- Fiber primitives
- Processes — Erlang-style processes, GenServer, and supervisors built on fibers