vihaco / virtual ISA & machine framework
Guides
  1. Defining Instructions
  2. Advanced Instruction Usage
  3. Parser Integration
  4. Pattern Parser Generator
  5. Advanced Parser Customization
  6. Using Messages
  7. Building Components
  8. Observing Effects
  9. Defining a Composite

Pattern Parser

The pattern parser derives source parsers for syntax types. A pattern describes the concrete spelling of a value and binds parts of that spelling to Rust fields. The derive validates the description at compile time, then emits a vihaco_parser::Parse implementation. Instruction-class enums also receive a vihaco_parser::SurfaceInstruction implementation.

Use two attributes:

  • #[syntax_class(...)] on the enum or struct selects the role of the syntax.
  • #[pattern = "..."] on an enum variant, or on a struct itself, overrides the generated pattern.

A complete instruction example

use chumsky::Parser as _;
use vihaco_parser_derive::Parse;
use vihaco_parser::Parse as ParseTrait;

#[derive(Debug, PartialEq, Parse)]
#[syntax_class(instruction, head = "memory")]
enum MemoryInstruction {
    Halt,
    #[pattern = "'load $0"]
    Load(u32),
    #[pattern = "'store $0 `,` $1"]
    Store(u32, i64),
}

let halt = MemoryInstruction::parser()
    .parse("memory::halt")
    .into_result();
assert_eq!(halt, Ok(MemoryInstruction::Halt));

let load = MemoryInstruction::parser()
    .parse("memory::load 4")
    .into_result();
assert_eq!(load, Ok(MemoryInstruction::Load(4)));

let store = MemoryInstruction::parser()
    .parse("memory::store 7, -2")
    .into_result();
assert_eq!(
    store,
    Ok(MemoryInstruction::Store(7, -2)),
);

The head is a dialect namespace. The derive appends ::, so head = "memory" combines with the pattern token 'load to accept memory::load.

Every bound field is parsed with that field type’s vihaco_parser::Parse::parser(). Give domain-specific field syntax its own local enum or struct with #[derive(Parse)].

Syntax classes

Every type using pattern generation must declare a syntax class.

AttributeMeaningAdditional rules
#[syntax_class(instruction, head = "dialect")]An instruction in the dialect:: namespace.Every pattern starts with an instruction token such as 'load.
#[syntax_class(value)]A value expression.Instruction tokens are forbidden. Simple defaults are available.
#[syntax_class(type)]A type expression.Instruction tokens are forbidden and every variant or struct needs an explicit pattern.

Put #[syntax_class] on the enum or struct definition, never on a variant or field. An instruction head is required and is written without trailing ::.

Generated patterns

You can omit #[pattern] when the conventional syntax is sufficient. Names are the lowercase Rust variant or struct name; acronym boundaries are not split (HttpServer becomes httpserver).

Rust shapeGenerated patternAccepted source
instruction Halt'haltdialect::halt
instruction Move(i64, bool)‘move $0 , $1dialect::move 3, true
instruction struct Set { x: i64, enabled: bool }‘set $x , $enableddialect::set 3, true
value Nothing`nothing`nothing
value Number(i64)$03
value Wrapper { value: i64 }$value3

Defaults intentionally stop there:

  • A value with more than one field must spell out how those fields are separated.
  • A type always requires an explicit pattern.
  • A unit value defaults to its lowercase name, while a unit instruction defaults to its lowercase instruction token.

Explicit patterns may reorder fields. Bindings identify constructor fields, not capture order, so #[pattern = “$right , $left”] still constructs a named type using the correct field names.

Pattern grammar

The complete grammar is small:

pattern       = atom, { " ", atom } ;
atom          = instruction-token | binding | literal ;
instruction-token = "'", ascii-identifier ;
binding       = "$", (ascii-identifier | decimal-index) ;
literal       = "`", (ascii-identifier | "," | "@"), "`" ;

The atoms mean:

FormPurposeExample
'nameMatch an instruction mnemonic. It is not included in the constructed value.'load
$0, $1, …Parse and capture a tuple field by zero-based index.$1
$fieldParse and capture a named field. Raw Rust identifiers bind by their unraw name, so r#type uses $type.$address
`word`Match an exact, case-sensitive keyword.`before`
`,`Match a comma with punctuation-aware spacing.$0 , $1
`@`Match an at sign with punctuation-aware spacing.$0 @ $1

Only comma and at sign are currently supported as symbol literals. Arbitrary punctuation and quoted strings are not part of the pattern language.

Whitespace is part of the grammar

Pattern atoms must be separated by exactly one ASCII space inside the Rust string. Leading spaces, trailing spaces, repeated spaces, and tab separators are compile-time errors.

In source text, a normal boundary between atoms accepts one or more ASCII spaces—not tabs or newlines. Symbol literals adjust just one boundary so common punctuation looks natural:

  • `,` suppresses whitespace before itself but still requires whitespace after itself: $0 , $1 accepts 1, true, not 1 , true or 1,true.
  • `@` still requires whitespace before itself but suppresses whitespace after itself: $0 @ $1 accepts 1 @target, not 1@target or 1 @ target.

Keywords and symbols match exactly. The generated parser does not consume indentation, line endings, or comments; the surrounding module/list parser is responsible for those boundaries.

Field-shape invariants

The derive treats a pattern as a checked mapping from source captures to a Rust constructor:

  • Tuple structs and tuple variants use only numeric bindings.
  • Named structs use only named bindings.
  • A pattern cannot mix numeric and named bindings.
  • Every field appears exactly once. Missing and duplicate bindings are errors.
  • Numeric bindings must be in bounds; named bindings must name a real field.
  • Unit structs and variants cannot contain bindings.

These rules allow fields to appear in any source order without making construction ambiguous.

use chumsky::Parser as _;
use vihaco_parser_derive::Parse;
use vihaco_parser::Parse as ParseTrait;

#[derive(Debug, PartialEq, Parse)]
#[syntax_class(value)]
#[pattern = "$right `,` $left"]
struct Pair {
    left: i64,
    right: bool,
}

assert_eq!(
    Pair::parser().parse("true, 42").into_result(),
    Ok(Pair {
        left: 42,
        right: true,
    }),
);

Enums, structs, and dispatch

Pattern generation supports both enums and structs:

  • On an enum, put #[pattern] on each variant that needs an override.
  • On a struct, put its single #[pattern] on the struct definition.
  • Enum variants may be unit, tuple, or struct-style variants. Tuple variants use numeric bindings, while struct-style variants use named bindings.
  • Generic types and types that already use a lifetime named similarly to the derive’s internal lifetime are supported.

For instruction enums, alternatives are ordered by mnemonic length before emission. This prevents a short token such as v2 from consuming the prefix of v25. Pattern-generated enums also support more than chumsky’s 26-element tuple-choice limit; the derive groups large enums into shallow nested choices. Alternatives with equal-length instruction tokens, and value/type alternatives without instruction tokens, retain declaration order.

Compile-time validation

Errors point at the #[pattern = "..."] literal when possible. Validation runs in stages, so failures tend to describe the earliest broken contract:

StageExamples of rejected input
Pattern syntaxEmpty patterns; leading, trailing, or repeated spaces; tabs; malformed bindings; unsupported symbols; unterminated literals; indices larger than u32.
Syntax classMissing #[syntax_class]; instruction pattern not beginning with 'name; instruction tokens in value/type patterns; implicit type patterns; implicit multi-field value patterns.
Field mappingMixed binding styles; named bindings on tuple fields; indexed bindings on named fields; missing, duplicate, unknown, or out-of-bounds fields; bindings on unit forms.
Attribute placement#[pattern] used without a syntax class; #[syntax_class] placed on a variant or field instead of the type definition.

The generated parser then relies on Rust’s type checker to verify that every captured field type implements Parse<'src>.

Compose nested syntax

Patterns cover exact mnemonics and keywords, typed field parsers, comma, and @. They keep the source grammar visible in one string and checked against the Rust constructor.

Represent richer field grammar with another pattern-derived local enum or struct. Use Ident, BareToken, and QuotedString for lexical leaves, and use Vec<T> or tuples for collection shapes. Each binding composes the parser of its field type, keeping the complete source grammar typed and pattern-derived.

For module parsing, typed function bodies, resolution, and explicit source sugar, continue to Module Parsing and Resolution.

On this page
  1. A complete instruction example
  2. Syntax classes
  3. Generated patterns
  4. Pattern grammar
  5. Whitespace is part of the grammar
  6. Field-shape invariants
  7. Enums, structs, and dispatch
  8. Compile-time validation
  9. Compose nested syntax