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 composites

Once you have devices, a composite brings them together. Its declaration tells vihaco where to send instructions. The steps you write decide how the devices work with the rest of the machine.

Declare the devices

use vihaco::composite;

#[composite]
struct Machine {
    #[device(0)]
    #[program]
    execution: vihaco_stdlib::ExecutionState,
    completed_runs: usize,
}

Give each device an explicit u8 code that’s unique within its parent. Ordinary fields such as completed_runs hold application state and add no instructions. You can mark one device #[program] to select invocation-state access and connect operand storage.

To give a device another accepted source spelling, use an alias such as #[device(1, alias = "alu")]. Field routes still use the Rust field name. The device’s code, name, and aliases each have their own job.

Route to direct or nested devices

field!(Machine::execution) names a direct route as a type. You can keep going through a nested composite with a route such as field!(Machine::controller::calculator). Generated Field<F> implementations tell you the final child type and let you borrow it with get or get_mut.

Two fields can hold the same component type. They are separate destinations, and you can give them different step behavior. Nested composites contribute their devices’ full declared capabilities to the enclosing instruction set.

Generated instruction sets and dispatch

You no longer need to manually define an instruction enum for the whole machine. #[composite] lifts the subdevices’ declared instructions into the parent and flattens them into a combined instruction set, including routes through nested composites. Each instruction retains its destination route, so two devices that support the same dialect remain distinct destinations.

Use <Machine as HasInstructionSet>::Runtime for the combined executable instruction type, where Machine is your composite type, and <Machine as HasInstructionSet>::Syntax for its source form:

use vihaco::{HasInstructionSet, composite};

#[composite]
struct Machine {
    #[device(0)]
    execution: vihaco_stdlib::ExecutionState,
}

type Instruction = <Machine as HasInstructionSet>::Runtime;
type SourceInstruction = <Machine as HasInstructionSet>::Syntax;

Use those public associated types in your code so you don’t have to rely on generated enum names or variants.

In source, routes look like field::nested_field::dialect::mnemonic. Aliases add accepted spellings for a device without creating new instruction identities. A component’s separate executable association doesn’t narrow the capabilities it contributes to its parent.

For every included route and instruction, supply a Step<F, I, Context>. Once those steps are available, #[composite] provides Dispatch<Context>. Vihaco checks coverage when you use dispatch; declaring the struct alone doesn’t check that you’ve supplied all its execution behavior.

The enclosing composite owns the policy

When an outer composite dispatches to a nested device, it selects the outer composite’s Step. If you want to reuse the inner composite’s step behavior, delegate to it explicitly. Nesting the structs won’t do that automatically.

You also choose message preparation and effect handling, and construct images outside the dispatchable composite. A Loadable pairs an image with its machine.

There are a few naming details to keep in mind as you split code into modules. Declaration discovery needs named child types. Renaming a child import or type alias across a module boundary requires matching reexports of its generated inventory and field companion namespaces. Bare generic parameters and associated child types can’t supply an inferred inventory. Direct named imports are a good starting point.

For standard device fields, use the defining crate, such as vihaco_stdlib::ExecutionState or vihaco_cpu::CPU. A facade may reexport the type without its companion namespaces. You can still use the facade’s type name in ordinary bounds and method calls.

Next, follow one instruction through execution.

On this page
  1. Declare the devices
  2. Route to direct or nested devices
  3. Generated instruction sets and dispatch
  4. The enclosing composite owns the policy