Building Components With vihaco
Components are the basic execution units in vihaco.
You define:
- a component with
component!, including its state and instruction surface - an optional resolved message type
- an optional effect type
- one
#[dispatch(...)]impl that executes the component’s runtime instruction
This guide shows the current public authoring model for defining your own component.
If you want a focused guide to instruction enums, explicit instruction width, and nested composite-level wrappers, read Defining Instructions With vihaco.
If you want a focused guide to resolved execution input and composite-side message generation, read Using Messages With vihaco.
The Core Pieces
A component usually starts with a component! declaration and one or two supporting data types:
- a
component!declaration containing the component state and instructions - a message type with
#[derive(Message)]when execution needs pre-resolved input - one or more plain Rust effect types when execution needs to return output
Use them this way:
component!: the component state plus its syntax and runtime instruction typesMessage: resolved execution input delivered into the component for that stepEffect: value returned from execution and later interpreted by the runtime or delivered to observers
Example:
use eyre::Result;
use vihaco::{component, dispatch, Effects, Message};
component! {
#[derive(Debug, Default)]
pub component Counter {
value: i64,
}
instruction {
Add(i64),
Print,
}
}
#[derive(Debug, Clone, Message)]
pub struct PrintPrefix(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StdoutEffect(pub String);
use counter::runtime::Instruction as CounterInst;
use counter::Counter;
Defining #[dispatch(...)]
Component execution lives on an impl block annotated with #[dispatch(...)].
#[dispatch(instruction = counter::runtime::Instruction, message = PrintPrefix, effect = StdoutEffect)]
impl counter::Counter {
fn execute(&mut self, inst: &CounterInst, msg: PrintPrefix) -> Result<Effects<StdoutEffect>> {
match inst {
CounterInst::Add(v) => {
self.value += v;
Ok(Effects::none())
}
CounterInst::Print => Ok(Effects::one(StdoutEffect(format!(
"{}{}",
msg.0, self.value
)))),
}
}
}
The execution method shape is:
fn execute(&mut self, inst: &Inst, msg: Msg) -> eyre::Result<Effects<Effect>>
Important points:
Instmust match theinstruction = ...typeMsgmust match themessage = ...type- when
effect = ...is omitted, the effect type defaults to() - normal execution output is returned as
Effects<Effect>
It is useful to keep the data flow straight:
Messagegoes into a componentEffectcomes out of a component- components consume
Message - runtimes and observers consume
Effect
When To Use message = ()
Use message = () when the component can execute directly from its instruction and local state.
use eyre::Result;
use vihaco::{component, dispatch, Effects};
component! {
#[derive(Debug, Default)]
pub component Lamp {
on: bool,
}
instruction {
On,
Off,
}
}
use lamp::runtime::Instruction as LampInst;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LampChanged(pub bool);
#[dispatch(instruction = lamp::runtime::Instruction, message = (), effect = LampChanged)]
impl lamp::Lamp {
fn execute(&mut self, inst: &LampInst, _msg: ()) -> Result<Effects<LampChanged>> {
self.on = matches!(inst, LampInst::On);
Ok(Effects::one(LampChanged(self.on)))
}
}
Use a non-unit message when execution needs resolved data that should not be encoded directly in the instruction itself.
As a rule:
- use
Messagefor step-local execution input - use
Effectfor values the runtime should interpret or deliver after execution
Execution Surface
Component execution depends only on explicit inputs and returned effects.
InstructionandMessageare the full inputs toexecute(...)Effects<Effect>is the full output fromexecute(...)- runtimes decide how to interpret returned effects after execution
Component Instruction Types
component! defines the component and records the two instruction types that make up a component’s
public instruction surface. It implements vihaco::HasInstructionSet with:
Runtime = component_module::runtime::InstructionSyntax = component_module::syntax::Instruction
For example, a component named Counter exposes
counter::runtime::Instruction and counter::syntax::Instruction. The first
is used by execution and bytecode-facing composition; the second implements
Parse and SurfaceInstruction for source text.
The generated component syntax uses the component’s snake_case name as its dialect head; its parser accepts instruction
mnemonics such as counter.add. When the component is placed in a
#[composite], the composite adds the device field name (or a configured
alias) as an outer instruction head, producing source such as counter_a::counter.add.
The component! macro also marks the generated type as a Component, which
identifies an executable device. The attribute form, #[dispatch(...)], is intentionally separate. It wires
an execution implementation onto the component’s generated runtime instruction
type; component! supplies the HasInstructionSet and Component implementations.
Design Guidance
- Put bytecode-visible execution variants in the instruction enum.
- Put resolved execution input in the message type.
- Put follow-up outputs in plain effect types.
- Keep the component responsible for its own state mutation.
- Use
effect = StepOutcomewhen a component needs to return control-flow signals.
Returning A Custom Effect
By default, execute(...) returns Result<Effects<()>>. When a component needs to return a real effect, use the effect parameter:
use vihaco::{component, dispatch, Effects, Message};
use vihaco_cpu::StepOutcome;
component! {
pub component CpuCore {}
instruction {
Nop,
Halt,
}
}
use cpu_core::runtime::Instruction as CpuInst;
#[derive(Debug, Clone, Message)]
pub struct CpuMsg;
#[dispatch(instruction = cpu_core::runtime::Instruction, message = CpuMsg, effect = StepOutcome)]
impl cpu_core::CpuCore {
fn execute(&mut self, inst: &CpuInst, _msg: CpuMsg) -> eyre::Result<Effects<StepOutcome>> {
match inst {
CpuInst::Nop => Ok(Effects::one(StepOutcome::Continue)),
CpuInst::Halt => Ok(Effects::one(StepOutcome::Halt)),
}
}
}
The effect parameter is optional. When omitted, the macro sets type Effect = (). When present, the component’s GeneratedComponent::Effect type matches what you specify.
Important: effects only matter when some runtime continues them. In practice:
- Hand-written runtime code can call
execute_generateddirectly and extract the returned effects. For single-effect control flow,expect_exactly_one_effect(...)is the common helper. - When a runtime needs to mix control-flow effects with other follow-ups, it usually defines a runtime-local sum-effect enum, gathers those values, and continues them in one place.
- Transitional
#[composite]wiring generates component instruction types; continuing returned effects to observers is something the hand-written runtime does (see Defining A Composite Withvihaco), and it does not interpretStepOutcomefor you.
As a rule: use plain effect types for observer-delivered outputs, and use runtime-local sum-effect enums when a hand-written runtime needs extra per-step interpretation.
CPU Function Frames
The standard CPU reserves one indexed locals region for each invocation.
Parameters occupy its first slots; additional locals follow and start at zero.
Load/store address this region relative to Frame.base. Temporary operands begin
at Frame::operands_index(), which is base + local_count. Store cannot enlarge
the region, and operand operations cannot consume reserved locals.
During resolution, the composite scans its own function’s surface instructions and computes the local count required by its CPU. Counts include parameters and every referenced load/store slot, including unreachable instructions. The composite owns this logic because it knows how its generated instruction enum routes instructions to its devices.
The following example parses a function, records its requirements in
FunctionInfo, and prepares its entry frame. The local_count includes all
parameter slots, so it must be at least the function arity:
use chumsky::Parser as _;
use vihaco::{Parse, module::{FunctionInfo, Parameter, Signature}, syntax::ParsedFunction};
use vihaco::traits::{StackFrame, StackMemory};
use vihaco_cpu::{CPU, SurfaceType};
#[vihaco::composite]
struct Machine {
#[device(1)]
cpu: CPU,
}
let parsed = ParsedFunction::<machine::syntax::Instruction, SurfaceType>::parser()
.parse("fn @main(input: u64) -> u64 { cpu::cpu.load_u64 3 cpu::cpu.ret 1 }")
.into_result().unwrap();
let arity = u32::try_from(parsed.params.len())?;
// The composite's resolver computes this from its own surface instruction enum.
let count = arity.max(4);
let function = FunctionInfo {
name: 0,
signature: Signature {
params: vec![Parameter { name: 1, ty: vihaco::Type::U64 }],
ret: vec![vihaco::Type::U64],
},
local_count: count,
start_address: 0,
end_address: 2,
file: 0,
};
let mut machine = Machine { cpu: CPU::default() };
machine.cpu.stack_push(42_u64);
machine.cpu.enter_function(arity, function.start_address, function.local_count, Some(0))?;
assert_eq!(machine.cpu.stack(), &[42, 0, 0, 0]);
assert_eq!(machine.cpu.get_frame()?.operands_index(), 4);
assert_eq!(machine.cpu.take_pending_pc(), Some(function.start_address));
// For subsequent calls, the composite selects the target metadata and device.
let message = vihaco_cpu::CPUMessage::FunctionInfo {
arity,
start_address: function.start_address,
local_count: function.local_count,
};
Consumer resolvers retain responsibility for lowering the parsed body and
assigning function addresses. The composite supplies the resolved
local_count; if no additional locals are needed, it is exactly the function’s
arity. The CPU rejects a call or entry frame whose local count is smaller than
its arity.
Before a call, load locals or compute argument values onto the caller’s operand
stack. Direct Call(arity, address) requires a CPUMessage::FunctionInfo and uses
its selected local count. Indirect calls receive arity, address, and local count
through that message; only the function reference, above the arguments, is popped
from the operand stack. Metadata is never pushed as operand words.
enter_function implements frame setup for entry and both call forms. It reuses
the argument operand slots and appends zero-filled additional locals. Return(n)
preserves the top n operands, removes the rest of the frame, and restores the
caller. With no caller, results are available through CPU::return_values().
The composite continues to own program-counter routing and instruction execution.
What Comes Next
Once you have one or more components, the next step is to understand how observer types consume the returned effects.
Continue with Observing Effects With #[observe].