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

Emitting SST from a compiler

SST’s future: SST may become unsupported or be superseded by a bytecode format plus a disassembler. It is not a promised long-term compiler target. The binary format is also currently unstable; this warning does not imply a stable replacement is already available.

A compiler targeting SST needs both a target machine’s instruction vocabulary and the program metadata expected by its loader. SST provides section framing; the target supplies instruction, type, value, and header grammars. Keep the compiler and loader on the same vihaco revision and agree on application-specific declarations and lowering rules.

File and section structure

Emit sst v1 first, followed by an optional .global: / .global. block and exactly one root section named root. Put structural markers on their own lines. This is a complete program for a machine with an execution device using the standard program dialect:

sst v1
.section(root):
    .header(root):
        metadata {
            strings: ["main", "output.sst"],
            functions: [
                {
                    name: 0,
                    signature: { params: [], ret: [u64] },
                    local_count: 0,
                    start_address: 0,
                    end_address: 2,
                    file: 1,
                },
            ],
            labels: [],
            constants: [],
            main_function: 0,
            source_symbols: [],
            extra: {},
        };
    .header(root).
    .text(root):
        fn @main() -> u64 {
            execution::program::const_u64 42
            execution::program::ret 1
        }
    .text(root).
.section(root).

Each section may contain one header block, one text block, and nested .section(child): / .section(child). blocks. Header and text marker names must match their containing section’s local name. Sibling section names must be unique. A section without header or text yields an empty block to the consumer; a runnable program still needs the content its loader requires.

Sections identify independently loadable units, not instruction destinations. All program tables and instruction addresses below are local to the selected section. A .global block has a consumer-defined grammar; omit it for loaders using NoContext. Structural framing is line-based; use // comments inside headers and function bodies rather than appending them to marker lines.

Target instruction and function syntax

The target composite’s <Machine as HasInstructionSet>::Syntax determines which instructions the loader accepts. Emit the device route, dialect, and mnemonic, followed by the operands specified by that instruction’s pattern. For example, calculator::arithmetic::add routes to the calculator device. A nested device adds route segments; device aliases may provide additional accepted spellings. Numeric #[device(code)] values are not the source spelling.

Emit functions as fn @name(param: type, ...) -> type { ... }. Omit -> for no returns, or use -> (type, type) for multiple returns. The target type parser defines accepted type names. Function parameters and return types must agree with the header metadata.

Inside a function, label @name declares a local branch target. It occupies no instruction slot. Instructions such as execution::program::br @done can refer forward to a label. Symbolic operands and their punctuation are dialect-specific; see the parser pattern reference.

Each parsed instruction lowers to exactly one runtime instruction. Expand any compiler pseudo-instructions before calculating ranges and emitting SST. Concatenating function bodies in source order defines the section’s code vector.

The metadata header

The standard loading path uses HeaderDeclarations<ProgramMetadata> or an application declaration enum containing ProgramMetadata. Declarations end in ;. The first-machine loader requires exactly one metadata { ... }; record; other loaders choose their own required declarations and duplicate policy.

Emit every field below, including empty arrays. Records use named fields and comma-separated lists; trailing commas are accepted. The standard schema is ProgramMetadata<Value, Type, NoInfo>:

FieldWhat the compiler supplies
stringsAn ordered array of quoted strings, including function names, parameter names, filenames, label names, and literals needed by lowering. String indices are zero-based.
functionsAn ordered array of function records, described below. Array position is the function index; this order may differ from function-body order.
labelsRecords { name: string_index, address: instruction_address }. Every source label needs a matching name and absolute section-local address.
constantsAn ordered array of values parsed by the target’s value parser. Constant indices are array positions.
main_functionThe entry function’s table index, or null when no entry is selected. It is not an instruction address.
source_symbolsRecords { index: number, name: "symbol" }. Indices must be unique; names are inline strings. Agree with the consumer on their meaning.
extraApplication metadata in the target’s chosen schema. With the default NoInfo, emit {}.

Each function record contains:

FieldMeaning
nameIndex of the function name in strings, without the source @ prefix. Function names must be unique for standard metadata-backed lookup.
signature{ params: [{ name: string_index, ty: type }, ...], ret: [type, ...] }. Parameter names, order, and types and return types must match the source declaration.
local_countTotal reserved local slots, including parameters. Must be at least the parameter count; the loader does not infer extra locals.
start_addressIndex of the first instruction in this function’s body in the concatenated code vector.
end_addressExclusive end of that body’s instruction range.
fileIndex of the source filename in strings.

Addresses and table references use u32. Count decoded instructions, not bytes, source lines, or labels. In the example, main covers 0..2. In a two-function program whose bodies have six and two instructions, their ranges are 0..6 and 6..8, even if their metadata records appear in reverse order. Calls that lower to function indices follow metadata table order.

Constants, strings, and symbol resolution

With standard Value, constants use typed literals such as u64 42, i64 -1, bool true, string 0, and fn 0. String values reference strings; function references select functions. The loader checks these references when assembling the module. Use f32_bits followed by a decimal u32 or f64_bits followed by a decimal u64 to preserve exact floating-point bits, including NaNs. Custom value types can define a different grammar.

Populate strings before lowering. MetadataLoweringContext looks up strings without appending or reordering them; a missing literal is an error. Equal strings resolve to the first matching table entry. Function lookup preserves the supplied function indices. Label lookup is scoped to the current function, so different functions can reuse a label name. Its resolved address is still absolute within the section.

Application header directives can describe device configuration or add lowering capabilities. These require an explicit agreement with the consuming loader; the standard metadata record does not define their schema. See loading SST programs for declaration preparation and custom contexts.

Validate compiler output

Load emitted files through the same path as the target application: SstFile::from_text, ParsedModule::parse_section, declaration preparation, MetadataLoweringContext::new, lower_functions_with_metadata, then metadata.into_module(code). This checks function declarations against the supplied tables, verifies source labels and instruction ranges, resolves symbols, and checks standard constant references. Loading preserves table order and does not invent missing metadata or select an entry function for you.

For the minimal file above, this loader validates and assembles the program:

use vihaco::{HasInstructionSet, NoContext, SstFile, Type, composite};
use vihaco::loader::MetadataLoweringContext;
use vihaco::module::ProgramMetadata;
use vihaco::syntax::{HeaderDeclarations, ParsedModule, lower_functions_with_metadata};

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

fn validate(source: &str) -> eyre::Result<()> {
    type SourceInstruction = <Machine as HasInstructionSet>::Syntax;
    let file = SstFile::<NoContext>::from_text(source)?;
    let ParsedModule { header, functions } = ParsedModule::<
        SourceInstruction, Type, HeaderDeclarations<ProgramMetadata>
    >::parse_section(file.root())?;
    let mut declarations = header.into_iter();
    let metadata = declarations.next().ok_or_else(|| eyre::eyre!("missing metadata"))?;
    eyre::ensure!(declarations.next().is_none(), "duplicate metadata");
    let mut context = MetadataLoweringContext::new(&metadata)?;
    let code = lower_functions_with_metadata(functions, &metadata, &mut context)?;
    let module = metadata.into_module(code)?;
    assert_eq!(module.code.len(), 2);
    assert_eq!(module.main_function, Some(0));
    Ok(())
}

Use your first machine as a minimal consumer and the Loadable example for multiple sections, reordered function metadata, a helper call, and a forward branch. Test both accepted programs and malformed output: missing strings, mismatched signatures or ranges, insufficient locals, unresolved labels, and invalid table references. Execute accepted programs to check behavior as well as syntax.

Continue with binary formats for encoded programs and compatibility planning.

On this page
  1. File and section structure
  2. Target instruction and function syntax
  3. The metadata header
  4. Constants, strings, and symbol resolution
  5. Validate compiler output