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

Parsing source

A parser recognizes how an instruction is written. Lowering turns the parsed operands into values you can execute. You need both when the source contains names, literals, or references that parsing alone can’t resolve.

Parse a leaf instruction

Here’s a parser for a single instruction:

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

use vihaco::{Parse, chumsky::Parser as _, dialect};

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

fn main() {
    let instruction = counter::Add::parser()
        .parse("add 5")
        .into_result()
        .expect("valid add instruction");
    assert_eq!(instruction.0, 5);
}

A leaf parser accepts the local mnemonic. A composite’s syntax parser adds the device route and dialect name, as in calculator::arithmetic::add. Use <Machine as HasInstructionSet>::Syntax for that combined parser.

Define a source pattern

#[pattern = "'name $0"] gives an instruction its mnemonic and captures a value for a field. Without an annotation, the binding uses the field’s parser. Add an annotation when you want to keep a different type during parsing:

use vihaco::{Lower, Parse, Syntax, chumsky::Parser as _, dialect};

#[dialect]
mod control {
    #[instruction]
    #[pattern = "'jump $0:u64"]
    pub struct Jump(pub u32);
}

impl<C: ?Sized> Lower<C> for control::Jump<Syntax> {
    type Runtime = control::Jump;
    fn lower(self, _: &mut C) -> eyre::Result<Self::Runtime> {
        Ok(control::Jump(self.0.try_into()?))
    }
}

let source = control::Jump::<Syntax>::parser()
    .parse("jump 4294967296").into_result().unwrap();
assert!(source.lower(&mut ()).is_err());

Here the syntax keeps a u64 until lowering checks whether it fits the runtime u32. Vihaco won’t insert a Rust conversion implicitly. Write your lowering impl outside the dialect module, in the same crate.

Resolve symbolic input

For a named branch target, keep the name in the syntax operand and ask the context for label lookup during lowering. For a quoted string, ask it for string interning. A local literal wrapper can also parse a readable number and encode it as a word while preserving its bits.

The generated aggregate requires the lowering capabilities of all its included instructions. You’ll need to supply those even if one program only uses numeric operands.

Each parsed instruction lowers to one runtime instruction. Function-local source labels become metadata, so they don’t add instructions. If you want instruction expansion, do that in a producer before this pipeline.

The pattern parser reference covers dialect instruction patterns and parsers for metadata, values, and types, with delimiter rules and diagnostics. Loading SST programs uses these parsers with complete programs.

On this page
  1. Parse a leaf instruction
  2. Define a source pattern
  3. Resolve symbolic input