vihaco / virtual ISA & machine framework
Author’s guide
  1. Start here
  2. The mental model
  3. Your first machine
  4. Essential concepts
  5. Dialects and instructions
  6. Writing components
  7. Preparing messages
  8. Stack inputs and outputs
  9. Writing composites
  10. Executing one instruction
  11. Handling effects
  12. Running programs
  13. Images and Loadables
  14. Parsing source
  15. Loading SST programs
  16. Emitting SST from a compiler
  17. Binary formats
  18. Patterns for larger machines
  19. Testing and extending
  20. Pattern parser reference

Writing components

A component is where a device keeps its state and does its work. You give it instructions and runtime input, and it returns a result. The surrounding composite takes care of supplying that input and deciding what to do next.

Declare support and implement execution

This counter adds an encoded increment to its own value:

// SPDX-FileCopyrightText: 2026 The vihaco Authors
// SPDX-License-Identifier: MIT

use vihaco::{Execute, NoMessage, component, dialect};

#[dialect]
pub mod counter {
    #[instruction]
    pub struct Add(pub u64);
}

#[component]
#[instructions { counter }]
#[derive(Default)]
pub struct Counter(u64);

impl Execute<counter::Add> for Counter {
    type Message = NoMessage;
    type Effect = u64;
    type Error = eyre::Report;

    fn execute(&mut self, instruction: &counter::Add, _: NoMessage) -> eyre::Result<u64> {
        self.0 = self
            .0
            .checked_add(instruction.0)
            .ok_or_else(|| eyre::eyre!("counter overflow"))?;
        Ok(self.0)
    }
}

let mut counter = Counter::default();
assert_eq!(counter.execute(&counter::Add(5), NoMessage::new())?, 5);

#[component] keeps your ordinary Rust struct and marks it as a component. #[instructions { counter }] says it supports the whole counter dialect. For each instruction, you choose the associated types in Execute<I>:

Associated typeWhat you choose
MessageThe runtime input the caller must supply
EffectThe result of successful execution
ErrorThe failure type, usually eyre::Report

Execution borrows the instruction and takes the message by value. This counter uses NoMessage because the instruction already contains everything it needs. Construct it with NoMessage::new(); it is a different type from ().

Choose the component boundary

Keep the device’s state and operation behavior in the component. Let the composite decide where operands come from and where results go. Then you can call the component directly in a test, or put it in a different composite, without changing its execution code.

The Calculator in your first machine receives PopFromStack<(u64, u64)>. Despite the wrapper’s name, the calculator gets owned numbers with no stack borrow. Its PushToStack<(u64,)> result works the same way: the caller chooses where and when to apply it.

Effects are instruction-specific

An instruction’s Execute<I>::Effect can be (), a scalar, a domain record, a stack result, or Effects<E> for several outputs. You don’t have to wrap every result in Effects<E>. The method returns Result<Self::Effect, Self::Error>, and each instruction can choose different message and effect types.

Return an error when the operation can’t fulfill its contract. Any changes made before the error stay in place. If an instruction promises to leave state alone on failure, check its inputs before changing state and test that promise.

Reuse a vocabulary across devices

You can write several components for the same dialect. A generic wrapper can also forward Execute<I> to an inner implementation, as long as its bounds establish the required message, effect, and error types.

Vihaco checks dialect coverage under the component declaration’s bounds. Those bounds must therefore support every instruction the component advertises.

Next, prepare the messages your component will receive.

On this page
  1. Declare support and implement execution
  2. Choose the component boundary
  3. Effects are instruction-specific
  4. Reuse a vocabulary across devices