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

Dialects and instructions

A dialect is a named vocabulary of instructions. It says what each operation means, and components implement those operations. That gives a simulator, a recorder, and a device connected to hardware a vocabulary they can share.

Declare the vocabulary

use vihaco::dialect;

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

    #[instruction]
    pub struct Clear;
}

Mark each operation type with #[instruction]. You can keep helper items in the same module; they won’t become instructions unless you mark them too.

When you document an operation, explain its payload fields, runtime inputs, and results, along with any behavior callers need to agree on. For arithmetic, for example, say whether overflow wraps or returns an error.

The macro gives components and composites the instruction identities and parsing support they need. You work with the public instruction types; vihaco handles the generated inventory and descriptions.

Decide what an operand represents

Some inputs belong in the program itself. counter::Add(5), for instance, encodes the increment as its payload. Other inputs arrive when the instruction runs. An addition with no payload can receive two numbers through PopFromStack<(u64, u64)>. Which choice fits depends on what the operation means.

A composite chooses the destination device. In source, controller::counter::counter::add 5 names the device fields first, then the dialect and mnemonic. A parser for just the instruction accepts the local spelling, add 5.

Syntax and runtime operands

Sometimes you want to keep a source name or readable literal around until the loader can resolve it. The dialect supplies the parsed and executable forms for you. Annotate a pattern binding when its source type differs from the value execution needs.

You write a Lower<C> implementation for fields that need conversion, using the lookup or other capabilities provided by the loading context. The impl names the parsed input and executable output; generated lowering carries unchanged values through for you. Parsing source has a checked example.

Consume or evolve a dialect

A component declares support for whole dialects and implements every instruction in each one. If you add an instruction, every component declaring that dialect needs an implementation for it. An extra Execute<I> impl on its own doesn’t add the dialect to the component’s declared capabilities.

It’s useful to keep the different names and identities straight. A mnemonic change affects source programs; a serialized opcode or payload change affects binary compatibility. Rust types and device routes have their own identities, and importing a dialect under an alias doesn’t rename its canonical identity.

With the vocabulary defined, you can write a component to execute it.

On this page
  1. Declare the vocabulary
  2. Decide what an operand represents
  3. Syntax and runtime operands
  4. Consume or evolve a dialect