Skip to content

feat(ppvm-vihaco): add the composite machine and VM#174

Open
david-pl wants to merge 14 commits into
mainfrom
david/42.3-composite
Open

feat(ppvm-vihaco): add the composite machine and VM#174
david-pl wants to merge 14 commits into
mainfrom
david/42.3-composite

Conversation

@david-pl

@david-pl david-pl commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Merge order

Part of the #168 split. This PR targets #173 (david/42.2-circuit-component) and sits at the top of the vihaco line:

  1. RotXY / R gate (feat(gates): add RotXY in-plane single-qubit rotation (R gate) #170) → main ✅ merged
  2. tableau expectation/reset (feat(tableau): add Pauli expectation/trace and reset_all #172) → main ✅ merged
  3. vihaco-circuit-isa (feat(vihaco-circuit-isa): add standalone circuit ISA crate #169) → main (open; carries feat(gates): add RotXY in-plane single-qubit rotation (R gate) #170 + feat(tableau): add Pauli expectation/trace and reset_all #172 via a main merge)
  4. native .pyi stubs (feat(python-native): add type stubs for the native _core module #171) → main (open; independent)
  5. circuit component (feat(ppvm-vihaco): add circuit component crate #173) → feat(vihaco-circuit-isa): add standalone circuit ISA crate #169
  6. composite machine + VM (this PR)feat(ppvm-vihaco): add circuit component crate #173
  7. CLI + TUI (Add a composable ratatui TUI to ppvm-cli #166) → this PR

Summary

Completes the ppvm-vihaco crate on top of the circuit component (#173) — the composite machine that drives it plus the surrounding VM.

File What
composite.rs the PPVM composite: vihaco machine (CPU + program loader) driving the circuit component, measurement/trace observers, backend selection from the device header, reset, single-instruction stepping
syntax.rs .sst header parse / lower (PPVMHeader, PPVMResolver)
bytecode.rs .ssb bytecode emit / load (magic-tagged)
observable.rs observable-header parsing for the PauliSum backends
shots.rs multi-shot execution (serial + optional rayon)
lib.rs full module wiring + load/run/dump/compile/parse helpers + PPVMModule
Cargo.toml adds vihaco-cpu, log, optional rayon feature
tests/*.sst + sst_fixtures.rs 28 integration fixtures

device_info relocation resolves here

composite.rs no longer defines PPVM_MAGIC / BackendKind / PPVMDeviceInfo — it imports them from device_info (introduced in #173) and pub use-re-exports them, so the existing crate::composite::{…} paths in lib.rs / bytecode.rs / syntax.rs keep resolving with no other edits.

The machete-ignore added in #173 is dropped here — the full crate uses chumsky / vihaco-parser-core directly.

Testing

cargo test -p ppvm-vihaco111 unit + 28 integration tests, green with and without --features rayon. Full workspace builds; clippy + machete clean.

david-pl and others added 5 commits July 8, 2026 10:01
Add the leaf `vihaco-circuit-isa` crate — the `CircuitInstruction`
enum plus its `CircuitEffect`/`CircuitMessage` companions — as a pure
workspace addition. Snapshotted from the tip of
`david/42-circuit-component` (PR #168) so it carries the public
`vihaco 0.1.1` dependencies rather than the crate's early
local-path/`0.1.0` history.

The crate is a leaf: nothing on this branch depends on it yet, so it
cannot regress existing code. It is the base of the circuit-component
split (see #168): merge order is core rotations -> Pauli trace/reset
-> this crate -> circuit component -> composite -> CLI/TUI (#166).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Introduce the `ppvm-vihaco` crate with just the circuit component slice:

- `component.rs` — the `Circuit` component: dispatches
  `vihaco_circuit_isa::CircuitInstruction`s to a tableau or PauliSum backend,
  emitting measurement / trace effects (includes the R gate and trace/reset
  dispatch now that RotXY and tableau expectation/reset are on `main`).
- `measurements.rs` — measurement / trace effect + observer types.
- `device_info.rs` — `PPVM_MAGIC`, `BackendKind`, `PPVMDeviceInfo`, relocated
  out of the composite so `component` compiles without the composite machine
  (which lands in a follow-up PR).
- `lib.rs` — module wiring for the component-only slice; the composite-driven
  helpers (load/run/dump/parse) come with the composite.

`chumsky` / `vihaco-parser-core` are transitive through the
`vihaco_parser::Parse` derive on `BackendKind`, so they carry a machete ignore.

Includes standalone smoke tests for the tableau backend (single-qubit flip and
CNOT propagation via the component's instruction dispatch); the fuller
integration coverage arrives with the composite's `.sst` fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the `ppvm-vihaco` crate on top of the circuit component:

- `composite.rs` — the `PPVM` composite: the vihaco machine (CPU + program
  loader) driving the circuit component, measurement/trace observers, backend
  selection from the device header, resets, and single-instruction stepping.
  Imports `BackendKind` / `PPVMDeviceInfo` / `PPVM_MAGIC` from `device_info`
  and re-exports them so existing `crate::composite::{…}` paths keep resolving.
- `syntax.rs` — `.sst` header parsing / lowering (`PPVMHeader`, `PPVMResolver`).
- `bytecode.rs` — `.ssb` bytecode emission / loading (magic-tagged).
- `observable.rs` — observable-header parsing for the PauliSum backends.
- `shots.rs` — multi-shot execution (serial + optional rayon parallelism).
- `lib.rs` — full module wiring plus the `load`/`run`/`dump`/`compile`/`parse`
  helpers and `PPVMModule` type.
- `Cargo.toml` — adds `vihaco-cpu`, `log`, and the optional `rayon` feature.

Includes the `.sst` integration fixtures (bell, GHZ, function calls, branching,
trotter truncation, loss/trace, rotations) and the composite half of the
PauliSum reset.

Tests: `cargo test -p ppvm-vihaco` — 111 unit + 28 integration; green with and
without `--features rayon`. clippy + machete clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://QuEraComputing.github.io/ppvm/pr-preview/pr-174/

Built to branch gh-pages at 2026-07-08 13:37 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the ppvm-vihaco crate by adding the composite PPVM machine/VM layer on top of the existing circuit component, plus end-to-end tooling (text syntax, bytecode format, observable parsing, and multi-shot execution) and a substantial .sst fixture suite to exercise the public API.

Changes:

  • Introduces the composite::PPVM machine wiring (vihaco-cpu + circuit component + observers) with stepping, file/program loading, and REPL-style single-instruction execution.
  • Adds .sst parsing/lowering (syntax.rs) and .ssb bytecode framing with magic/version/device-info (bytecode.rs), plus observable-header parsing (observable.rs).
  • Adds shot runner utilities (shots.rs) and 28-ish end-to-end .sst fixtures with an integration harness (sst_fixtures.rs).

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/ppvm-vihaco/src/composite.rs New composite PPVM machine: loader+CPU+circuit, stepping/run, observers, REPL instruction injection, device init.
crates/ppvm-vihaco/src/syntax.rs .sst header + instruction parsing and module resolution (labels/branches/calls, string table lowering).
crates/ppvm-vihaco/src/bytecode.rs .ssb container format read/write for resolved modules (magic/version/device header + strings + code).
crates/ppvm-vihaco/src/observable.rs Parser for device circuit.observable into Pauli-sum terms used by PauliSum backends.
crates/ppvm-vihaco/src/shots.rs Serial/parallel multi-shot execution API and deterministic per-shot seeding.
crates/ppvm-vihaco/src/lib.rs Public module wiring and helper APIs (parse/compile/load/run/dump) plus PPVMModule alias.
crates/ppvm-vihaco/Cargo.toml Adds vihaco-cpu, log, optional rayon feature, and dependency wiring.
Cargo.lock Locks new deps (vihaco-cpu, log, rayon) into the workspace lockfile.
crates/ppvm-vihaco/tests/sst_fixtures.rs Integration harness that runs .sst fixtures (and dump→load round-trips) via public PPVM APIs.
crates/ppvm-vihaco/tests/bell.sst Fixture: Bell prep + measurements.
crates/ppvm-vihaco/tests/hello_circuit.sst Fixture: small gate sequence without measurement.
crates/ppvm-vihaco/tests/rotxy.sst Fixture: circuit.r end-to-end (rotxy) + measurement.
crates/ppvm-vihaco/tests/function_call.sst Fixture: call into helper and return value plumbing (basic).
crates/ppvm-vihaco/tests/function_call_ret.sst Fixture: ret 1-style return-value workflow and branching in caller.
crates/ppvm-vihaco/tests/function_call_branch_both.sst Fixture: tri-state outcome branching (incl. loss) with helper returning an outcome.
crates/ppvm-vihaco/tests/branch_on_outcome.sst Fixture: probabilistic branch steering with invariant check.
crates/ppvm-vihaco/tests/branch_on_outcome_x.sst Fixture: deterministic branch steering.
crates/ppvm-vihaco/tests/paulisum_bell_trace.sst Fixture: PauliSum backend Bell ⟨ZZ⟩ trace via Heisenberg order.
crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst Fixture: PauliSum backend GHZ ⟨XXX⟩ trace via Heisenberg order.
crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst Fixture: PauliSum backend non-Clifford RY trace behavior.
crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst Fixture: PauliSum backend multi-term observable seeding + trace.
crates/ppvm-vihaco/tests/paulisum_measure_error.sst Fixture: PauliSum backend measure rejection behavior.
crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst Fixture: PauliSum Trotter + explicit truncate between layers.
crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst Fixture: LossyPauliSum loss-channel + trace.
crates/ppvm-vihaco/tests/tableau_bell_trace.sst Fixture: Tableau backend trace against positional Pauli word.
crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst Fixture: Tableau backend GHZ trace.
crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst Fixture: Tableau backend non-Clifford RY then trace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ppvm-vihaco/src/observable.rs Outdated
Comment on lines +93 to +97
// Term: coefficient [*] word | bare word.
let term_with_coeff = coefficient
.then_ignore(just('*').padded().or_not())
.then(pauli_word)
.map(|(c, w)| (w, c));
Comment thread crates/ppvm-vihaco/src/shots.rs
Comment on lines +338 to +350
/// Append one REPL command's lowered VM ops and run just that block against
/// the persistent state, advancing the pc through it. NOTE: an out-of-range
/// qubit index *panics* in the tableau rather than erroring, so callers must
/// bounds-check qubit operands against `n_qubits` first.
pub fn execute_single_instruction(&mut self, instrs: &[PPVMInstruction]) -> eyre::Result<()> {
let start = self.loader.module.code.len() as u32;
self.loader.module.code.extend_from_slice(instrs);
*self.loader.pc_mut() = start;
for _ in 0..instrs.len() {
self.step_once()?;
}
Ok(())
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#166 already has a fix for this and it will be merged afterwards, so this is not a concern here so long as we merge #166 soon after this PR.

Comment thread crates/ppvm-vihaco/src/composite.rs Outdated
Comment on lines +185 to +199
// Don't pre-allocate from an untrusted count; grow as entries are read.
let string_count = read_u32(r)?;
let mut strings = Vec::new();
for _ in 0..string_count {
let len = read_u32(r)? as usize;
let mut bytes = vec![0u8; len];
r.read_exact(&mut bytes)?;
strings.push(String::from_utf8(bytes)?);
}

let code_count = read_u32(r)?;
let mut code = Vec::new();
for _ in 0..code_count {
code.push(PPVMInstruction::from_bytes(r)?);
}
Comment on lines +6 to 10
[features]
# Enables shot-level parallelism in `run_shots_parallel`/`run_shots`.
rayon = ["dep:rayon"]

[dependencies]
Comment on lines +3 to +5
// TODO: aspirational — depends on `ret <n>` restoring the caller's PC and
// leaving the top `n` values on the caller's stack. The runtime doesn't do
// this today (Frame has no return_pc), so this fixture currently fails.
david-pl and others added 8 commits July 8, 2026 14:34
The size-bucketed circuit constructors panicked when n_qubits exceeded the
widest executor bucket. Since n_qubits comes from user-supplied device
headers and this backend drives a TUI, a panic would tear down the terminal
rather than surface a friendly message. Return eyre::Result from the four
constructor pairs (Tableau/PauliSum/LossyPauliSum, plus new_with_seed) so
callers can report the error instead of aborting.

Add a MAX_QUBITS constant so the ceiling is single-sourced across the bucket
checks and error messages. Default stays infallible (0 qubits always fits the
smallest bucket).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…init_inner

The circuit constructors now return eyre::Result (merged from #173), so
PPVM::init_inner propagates with `?`. It already returns eyre::Result<()>
and reports device-header errors, so this just flows the construction error
(e.g. n_qubits > MAX_QUBITS) into the same path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The documented observable grammar allows `coefficient WS* pauli_word`, but
the parser only tolerated whitespace around an explicit `*`, so "0.5 ZZ"
failed to parse. Consume optional whitespace before the Pauli word to match
the grammar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously ppvm-tableau's rayon was force-enabled on every non-wasm build via
a target-specific dependency, so downstream could not disable parallelism and
the `rayon` feature only gated shot-level parallelism. Fold both into one
off-by-default `rayon` feature (dep:rayon + ppvm-tableau/rayon) and drop the
forced target override. Consumers opt in for parallelism (ppvm-cli already
does); off by default keeps the default build wasm-safe with no CI changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +234 to +238
pub fn module_to_bytes(module: &PPVMModule) -> eyre::Result<Vec<u8>> {
let mut buf = Vec::new();
write_module(module, &mut buf)?;
Ok(buf)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth move to vihaco? @robpatterson13

}

/// Reconstruct a module from a byte slice.
pub fn module_from_bytes(bytes: &[u8]) -> eyre::Result<PPVMModule> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prob should move to vihaco as part of the public interface of Module too

cc: @robpatterson13


measurement_record: MeasurementObserver,

trace_record: TraceObserver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI. this is where the deq component needs to go eventually

cc: @rafaelha

fn pop_qubit(&mut self) -> eyre::Result<usize> {
match self.cpu.stack_pop()? {
vihaco::Value::U32(v) => Ok(v as usize),
vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into),

@Roger-luo Roger-luo Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe restrict the qubit address to u64 - technically I64 is illegal input to the instructions otherwise we will be making this weak typed.

Comment on lines +455 to +471
fn continue_effects(&mut self, effects: Effects<PPVMEffect>) -> eyre::Result<StepOutcome> {
let mut step_outcome = None;
for effect in effects {
match effect {
PPVMEffect::Step(outcome) => {
if step_outcome.replace(outcome).is_some() {
return Err(eyre::eyre!(
"expected exactly one PPVM step effect, got multiple"
));
}
}
effect => self.continue_observer_effect(effect)?,
}
}

step_outcome.ok_or_else(|| eyre::eyre!("expected exactly one PPVM step effect, got 0"))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks a bit redundant to me, maybe worth something to think about simplifying upstream?

cc: @robpatterson13

Comment on lines +571 to +592
pub fn load_bytecode(&mut self, bytes: &[u8]) -> eyre::Result<()> {
let module = crate::bytecode::module_from_bytes(bytes)?;
self.load(&module)
}

/// Read a `.ssb` file and load the module it contains.
pub fn load_bytecode_file(&mut self, path: &str) -> eyre::Result<()> {
let bytes = std::fs::read(path)?;
self.load_bytecode(&bytes)
}

pub fn run_program(&mut self, program: &str) -> eyre::Result<()> {
self.load_program(program)?;
self.run()?;
Ok(())
}

pub fn run_file(&mut self, path: &str) -> eyre::Result<()> {
self.load_file(path)?;
self.run()?;
Ok(())
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are also some helper traits worth considering upstream

cc: @robpatterson13

@@ -1,19 +1,118 @@
// SPDX-FileCopyrightText: 2026 The PPVM Authors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think helper functions in this file are also worth upstreaming to provide

cc: @robpatterson13

Base automatically changed from david/42.2-circuit-component to main July 13, 2026 15:43
Copilot AI review requested due to automatic review settings July 13, 2026 15:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 33 changed files in this pull request and generated 2 comments.

Comment on lines +31 to +34
#[token = "circuit.observable"]
#[delimiters(open = "", close = "", separator = "")]
Observable(#[parse_with = "vihaco_parser_core::ident"] String),

Comment on lines +188 to +193
for _ in 0..string_count {
let len = read_u32(r)? as usize;
let mut bytes = vec![0u8; len];
r.read_exact(&mut bytes)?;
strings.push(String::from_utf8(bytes)?);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants