Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

XQuad is a hardware-agnostic toolchain for expressing and running quadratic optimisation problems – QUBO, Ising, and integer formulations – on quantum annealers and classical solvers. A problem is written once against the XQVM instruction set and runs unchanged on any backend the toolchain supports.

Think of it as LLVM for quantum computing: write a problem once, compile it to XQVM bytecode, and run it on any supported backend.

What XQuad solves

Combinatorial optimisation problems – travelling salesman, graph colouring, knapsack, set cover, and their relatives – can be expressed as quadratic binary models and handed to a quantum annealer or a classical sampler. XQuad gives that pipeline a single intermediate representation, XQVM bytecode, and a reference virtual machine, so the same compiled program runs on a D-Wave QPU, a local simulated-annealing sampler, or the Quip network without a rewrite. See Quadratic Models for what a QUBO/Ising model actually is, and Backends for the solvers XQuad targets.

The real components

The toolchain is dual-language: a Rust core (VM, assembler, bytecode, CLI) with Python interfaces (reference VM, constraint-programming DSL, solver adapters, FFI bindings).

Three Rust crates, published to crates.io:

CrateBinaryRole
xqvmBytecode definitions, opcode table, instruction builder, binary codec, an incremental instruction-stream reader, the VM interpreter, and a disassembler
xqasmText assembler: .xqasm source to bytecode
xqclixquadThe unified CLI – xquad asm, xquad dism, xquad run, xquad verify

A fourth crate, xqffi, is a PyO3 bridge that exposes xqvm and xqasm to Python. It ships as a Python wheel rather than a crates.io library.

Five Python distributions, published to PyPI:

PackageRole
xqvm_pyPure-Python reference VM, used as the cross-implementation conformance oracle
xqcpHigh-level constraint-programming DSL that compiles to XQVM assembly
xqsaSolver adapters for XQMX models: local simulated annealing, D-Wave, and the Quip network
xqffiThe PyO3 FFI bindings crate above, packaged as a wheel
xquadUmbrella package re-exporting xqffi, xqcp, and xqsa under one namespace, with an interactive Program / Session / RunResult API

Parity on every committed conformance vector between the Rust xqvm interpreter and the Python xqvm_py reference VM is enforced mechanically: each vector runs on both implementations in CI, and disagreement fails the build. See Conformance for what a vector does and does not cover.

Architecture at a glance

XQVM is a stack-based interpreter with a 256-slot register file. The value stack holds i64 integers; registers hold typed values (RegVal): integers, integer vectors, QUBO/Ising/integer models (XqmxModel), model vectors, and candidate solutions (XqmxSample). A dedicated loop stack drives RANGE and ITER iteration.

The instruction set comprises 93 instructions, declared once in the opcodes! table at xqvm/src/bytecode/types/table.rs. The Opcode enum, the Instruction enum, mnemonic strings, and operand arity are all derived from that single table.

Every .xqb file opens with a fixed 15-byte XQBC header – magic bytes, format version, calldata/output-slot counts, instruction-stream length, and a CRC-32 checksum – followed by the raw instruction stream: an opcode byte followed by its operands in big-endian byte order. See Bytecode Format for the full field layout.

Where to go next

  • Getting Started – install the toolchain and run your first program.
  • Concepts – what a QUBO/Ising model is, the ways to use XQuad, and the backends it targets.
  • Modelling with XQCP – write a problem with the constraint-programming DSL.
  • Running Programs and Solving – execute a compiled program and hand it to a solver.
  • XQVM Reference – the machine model, assembly language, instruction set, and bytecode format.
  • Embedding – using the Rust crates directly, no_std support, and cross-implementation conformance.
  • Examples – worked problems including a Travelling Salesman Problem.

License

Licensed under the GNU Affero General Public License v3.0 or later.

Install XQuad

This page installs XQuad, confirms the install works, and gets the runnable examples this book uses. It also covers what a GPU or QPU solver needs beyond the base install. First Problem picks up from here with a real run.

Install

The root README’s Install section is the canonical install reference for both ecosystems this project ships into – prebuilt wheel platforms, the Rust toolchain required to build the xqffi extension from source, and installing individual Python packages instead of the umbrella. The two commands most readers need:

cargo install xqcli      # gives you the `xquad` binary
pip install xquad        # Python umbrella: the full pipeline

Verify the Install

xquad --version prints the installed CLI’s version, confirming the xqcli binary is on PATH and runs. Importing the umbrella package confirms the Python half:

$ python -c "import xquad; print('ok')"
ok

Get the Examples

Every uv run python examples/... command in this book, including on the next page, runs against a checkout of this repository – the examples are not part of any published package, so cargo install and pip install above do not put them on disk. Get one and set up the Python workspace once:

git clone https://gitlab.com/quip.network/xquad.git
cd xquad
make deps-py

make deps-py needs uv and the Rust toolchain cargo install xqcli above already needed. It syncs the Python workspace and wires up cross-package imports, so uv run python examples/<name>/runner.py then works from the repository root – the same command every example and cookbook page in this book uses.

GPU and QPU Install Prerequisites

The base install runs CPU simulated annealing only, through xqsa’s dwave-cpu backend. Every other solver – a local GPU or a real D-Wave QPU – needs an extra on top, and the extra needs hardware or credentials this page cannot install for you. xqsa/pyproject.toml defines the extras, and xquad/pyproject.toml forwards to the ones it re-exports:

  • cudacupy-cuda12x>=13.0, nvidia-cuda-nvrtc-cu12, and nvidia-cuda-runtime-cu12, for a local NVIDIA GPU.
  • dwavedwave-system>=1.0, for the real D-Wave QPU.
  • metalpyobjc-framework-Metal>=11.0, marked sys_platform == 'darwin', for a local Apple GPU. The marker means pip install xquad[metal] succeeds on Linux and installs nothing.
  • quipsubstrate-interface>=1.7.4,<2 plus quip-signer>=0.3.0,<0.4, for the Quip network solver, which needs a QUIP_RPC_URL and a configured signer; see Quip Network for what that solver does.

xqcp, xqffi, and xqvm_py define no optional dependencies at all.

The only extra this page can fully specify is [dwave]: it needs a D-Wave Leap account and a DWAVE_API_TOKEN, and dwave ping confirms both. A local GPU ([cuda] or [metal]) is another option; see Local Solvers for those two extras and their driver checks.

pip install xquad[cuda], xquad[metal], xquad[dwave], and xquad[quip] each forward to the matching xqsa extra, and extras are composable: pip install "xquad[cuda,dwave]".

A missing extra does not break the base install: import xqsa never fails just because an optional extra is absent. Only constructing the solver class that needs it does, raising ImportError with a pip install xqsa[...] hint naming the extra to add.

Per-solver parameters, driver-level troubleshooting, and what a QPU result contains that a CPU one does not are covered in Solving Overview, Local Solvers, and D-Wave QPU. This section only covers what to install before you get there.

Where to Go From Here

  • First Problem – run a complete problem end to end and get a real answer back.
  • What Happened – the explanation of what that run just did.
  • XQVM Reference – a minimal hand-written .xqasm program, if you want to see the machine underneath before running anything larger.
  • Toolchain Map – the pieces XQuad is built from, and how they hand off to each other.

First Problem

This page runs one problem from nothing to a real answer. It does not explain how the pieces work – What Happened does that once you have seen a result to explain.

Max-Cut

Max-Cut splits a graph’s nodes into two groups to maximise the total weight of edges crossing between them. Quadratic Models derives its formulation by hand on a three-node graph; examples/maxcut/ is the same problem, runnable, on a graph you pick the size of.

Run It

With XQuad installed and the examples checked out, per Install XQuad:

$ uv run python examples/maxcut/runner.py --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "cut_weight": 354,
  "energy": -354,
  "n": 5,
  "partition": [
    0,
    1,
    0,
    0,
    1
  ],
  "valid": 1
}

No flags beyond --seed are required: --n defaults to 5, and --solver defaults to dwave-cpu, the baseline that needs no hardware or credentials.

Reading the Result

partition is the answer: one 0 or 1 per node, five nodes here, naming which side of the cut each node is on. cut_weight is the total weight of edges connecting a 0-side node to a 1-side node – the quantity Max-Cut maximises. energy is -354, the negative of cut_weight, because the model minimises -1 times the cut weight to turn maximising into the minimising every XQuad model does; see Quadratic Models for why that sign flip is exact. valid: 1 confirms every value in partition is 0 or 1 – the only thing this problem’s verifier checks, since Max-Cut declares no constraint beyond that.

The Same Answer, a Different Machine

$ uv run python examples/maxcut/runner.py --seed 42 --interpreter rust

--interpreter picks which XQVM runs the compiled programs: the pure-Python reference VM (the default above) or the Rust interpreter. The encoder, verifier, and decoder are deterministic per interpreter – same bytecode in, same output out, on either one. The solve sitting between them is pinned only to the seed and the dwave-samplers version: solver.solve(model) runs simulated annealing, so a different version of that library can return a different valid sample for the same seed. For this seed the solve happens to land on the same sample either way, so cut_weight, energy, and partition also match above. Three Programs runs this exact seed and size on both interpreters side by side and says more about what is and is not guaranteed to match.

Next: What Happened names each step this run took, in order, and links to where each one is covered in full.

What Happened

First Problem ran examples/maxcut/runner.py and printed a partition, a cut weight, and an energy. This page is an index: it names the five moves that run made, in order, and points at the page that covers each one in full, rather than explaining them again here. Toolchain Map has its own diagram of the same pipeline, if a picture is what you want first.

Five Moves, One Command

  1. A problem became three programs. build_problem in examples/maxcut/runner.py describes Max-Cut once, in Python, with the xqcp DSL; problem.compile() turned it into an encoder, a verifier, and a decoder, sharing no state. Three Programs covers why there are three and not one, and walks this exact run step by step.
  2. The encoder ran and built a model. Quadratic Models covers the Hamiltonian it built, specialised to this graph’s weights.
  3. A solver minimised it. dwave-cpu, the default backend, ran simulated annealing over the model and returned a sample. Backends covers the other four solvers this same model could have gone to instead, unchanged.
  4. The verifier checked the sample and computed its energy independently. energy: -354 was not relayed from the solver; see Quadratic Models: Energy for that computation.
  5. The decoder turned the sample into partition. See Outputs and Decoding for what a decoder can read and what it hands back.

--interpreter rust in the previous page swapped which machine ran steps 2, 4, and 5. Three Programs covers what is and is not guaranteed to match between the two interpreters when that swap happens.

Where to Go From Here

  • Toolchain Map – every piece named above, in one table, with what each hands to the next.
  • Modelling – write your own Problem instead of reading someone else’s; builds on a second running example, Knapsack, from the first input declaration onward.
  • Running Programs – drive an already-compiled program from Python yourself, the way runner.py does internally.
  • Solving Overview – the five solvers behind step 3, and what each needs installed before you can reach it.
  • Using the Examples – turn Max-Cut, or any of the other thirteen examples, into a problem of your own.

Toolchain Map

XQuad turns one problem description into bytecode that runs unchanged on any backend it supports. This page names the pieces that make that true, and what each one consumes and produces.

flowchart LR
    P["DSL (xqcp) or hand-written .xqasm"] --> ASM[Assembler]
    ASM --> B[XQVM bytecode]
    B --> V["VM: xqvm or xqvm_py"]
    H[Host program] -->|calldata| V
    V -->|model-building program| M[XQMX model]
    M --> H
    H --> SLV[xqsa solver]
    SLV -->|sample| H
    H -->|sample as calldata| V
    V -->|verifier / decoder programs| H
ComponentRoleHands off
DSL (xqcp) or .xqasm sourceDescribes the problem: a program whose job is to build a model, in Python or in assembly text.xqasm source
Assembler (xqasm)Parses .xqasm text and resolves labelsXQVM bytecode (.xqb)
BytecodeThe portable artifact: identical bytes run on the Rust xqvm interpreter or the Python xqvm_py reference VMInput to the VM
VM (xqvm / xqvm_py)Executes bytecode against calldata the host program suppliesAn XqmxModel, when the program’s job is building one; decoded output, when its job is reading a sample back
Model (XqmxModel)The Hamiltonian a model-building program assembled: the model’s energy function, covered in Quadratic ModelsInput to a solver
Solver (xqsa)Minimises the model’s HamiltonianAn XqmxSample: the best assignment found
Host programThe script or CLI session driving every stage aboveReads the model out of the VM and hands it to the solver, then feeds the sample back in as calldata (the numbered input slots a host fills before each VM run) for the next run

Following the Pipeline

A hand-written program can skip most of this. Save the following as add.xqasm:

; push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
$ xquad asm add.xqasm -o add.xqb
assembled 4 instructions (21 bytes) -> add.xqb
$ xquad run add.xqb
stack (bottom to top):
  42

Here the pipeline is just two rows: the assembler and the VM. The xquad CLI plays the host program’s role, reading add.xqb from disk and printing the result, and that already runs the real toolchain end to end. XQVM Reference walks through this same add.xqasm again, from the machine’s side: what each instruction does to the stack as it runs.

A problem worth handing to a solver uses every row in the table above. xqcp compiles a Problem into three separate .xqasm programs, not one – see Three Programs for what those three are and why. The first one builds a model; xqsa solves it; the other two check and decode the answer. A Python host program – xquad.program, xqffi.vm, or a full xqcp pipeline – drives it, injecting calldata before each VM run and reading outputs back after. See Ways to Use XQuad for which surface plays that host-program role in each of the six ways to work with XQuad.

Where to Go From Here

  • Ways to Use XQuad – the six surfaces that build a model and drive execution, and how to pick one.
  • Quadratic Models – what the model in the middle of this pipeline actually is, and why constraints become part of it.
  • Three Programs – why xqcp compiles a problem into three separate programs instead of one.
  • Backends – the solvers that sit in the gap between encoder and verifier.

Past this chapter: Modelling builds a model with the DSL, Running Programs and Solving Overview execute it and hand it to a solver, and XQVM Reference is the machine this whole pipeline compiles down to.

Ways to Use XQuad

Six separate surfaces get a problem into XQuad and a result back out. Pick the wrong one and you find out several pages later. This page is the map.

SurfaceWhat it isWho it is for
xqcp DSLPython constraint-programming layer; compiles a problem to XQVM assemblyModelling a new combinatorial problem without hand-writing assembly
xquad Program/Session APIProgram / Session / RunResult: load a program once, run it repeatedly with fresh calldataDriving an existing compiled program from Python – a REPL, a notebook, a script
xqffi.vm FFIVm: a thin wrapper over the Rust interpreterPython code building its own convenience layer on top of the raw Rust VM
xquad.vm VM wrapperVM / VMBackend: runs .xqasm source text directly against either the Rust interpreter or the pure-Python reference VM, with xquad.types.XQMX at the boundaryBackend-parity checking, or working in xquad.types terms; the surface every example runner under examples/ actually uses
xquad CLIxquad asm / run / dism / verifyAssembling, running, inspecting, or verifying one .xqasm/.xqb file, no Python involved
Rust embeddingxqvm::InstructionBuilder, building a Program directly, no text assembly stepEmbedding XQVM in a Rust host: no_std target, WASM runtime, on-chain pallet

xqcp – The DSL

You write a Problem in Python: declare inputs, define the model, add objective terms and constraints, declare outputs. problem.compile() returns CompiledPrograms(encoder, verifier, decoder) – three .xqasm programs, ready to run. See Three Programs for what those three are and why there are three. Every example under examples/ is built this way; examples/maxcut/runner.py is a complete pipeline you can read start to finish.

Stop here if you have a combinatorial problem to model and no existing .xqasm source. Keep reading if you already have compiled bytecode and need to run or inspect it. Full coverage: Modelling.

xquad.program – Program and Session

Program.from_source(src) or Program.load(bytecode) loads a program once; program.session(output_slots=n) gives you a Session you call .run() on repeatedly, each time with fresh calldata via session.set_calldata(...). Each run returns a RunResult with dict-keyed outputs (an unset slot reads as None), the residual stack, and the step count. Program.from_source plus Session.run() round-trips 40 + 2 through calldata and an output slot to {0: 42}, and running the same session again returns {0: 42} again: each run() starts clean.

Stop here if you are driving an already-compiled program from Python and want calldata handling and output decoding done for you. Keep reading if you are choosing between this and the raw FFI below. Full coverage: Running Programs.

xqffi.vm – The Raw FFI

xqffi.vm.Vm is a thin wrapper over the Rust interpreter: construct a Vm, call set_calldata/set_output_slots, call .run(bytecode), read .outputs(). No Program/Session layer on top, and unlike Session, a Vm holds its stack and registers across runs: call .run() twice on the same Vm without an intervening .reset() and the second run’s results sit on top of the first’s. reset() clears the stack, registers, and loop state; calldata and outputs both survive a reset() untouched, and only a fresh set_output_slots(n) call replaces the output slots. Session sidesteps the question entirely: Session.run() builds a fresh Vm per call, so no state survives between runs. That is the real reason to prefer it for anything but a single one-off program.

Reach for Vm directly only if you are building your own convenience layer on top, or the Program/Session assumptions do not fit – most Python users want xquad.program or xquad.vm instead. Full coverage: Running Programs.

xquad.vm – Backend Dispatch

xquad.vm.VM selects between the Rust interpreter and the pure-Python reference VM via VMBackend, converting FFI objects to and from the canonical xquad.types.XQMX at the boundary, and runs .xqasm source text directly rather than pre-assembled bytecode. VMBackend.RUST is the default and reaches the Rust interpreter through xqffi.vm.Vm internally. Every example runner under examples/ imports VM and VMBackend from here, not xqffi.vm directly.

Stop here if you want backend-parity checking or your calldata and outputs are already in xquad.types terms. Keep reading if you need the raw FFI underneath instead. Full coverage: Running Programs.

xquad CLI

xquad asm, xquad run, xquad dism, xquad verify – four subcommands, entirely in the shell. Assemble a .xqasm file to bytecode, run a .xqb file (or a .xqasm file with --text) against calldata, disassemble bytecode back to a readable listing, or verify a compiled program before running it. xquad asm add.xqasm -o add.xqb followed by xquad run add.xqb, xquad dism add.xqb, and xquad verify add.xqb assembles, runs, disassembles, and verifies the four-instruction add.xqasm from Toolchain Map.

Stop here if you are working from a shell or from CI. Keep reading if you need to drive many runs programmatically rather than one at a time. Full coverage: CLI.

Rust Embedding

xqvm::InstructionBuilder builds a Program directly from Rust, with no .xqasm text step at all:

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Vm};

let mut builder = InstructionBuilder::new();
builder.emit_push(10).emit_push(32).emit_add().emit_halt();
let program = builder.build().unwrap();

let mut vm = Vm::new();
vm.run(&program).unwrap();
assert_eq!(vm.stack(), &[42]);
}

The xqvm crate is no_std + alloc-compatible, so this is the surface for embedding XQVM where Python, or even a filesystem, is not available.

Stop here if you are embedding the VM inside another Rust program. Everyone else wants one of the five surfaces above. Full coverage: Builder API.

The Orthogonal Choice: Where It Solves

Whichever surface builds your model, you still choose separately where that model gets solved: local CPU or GPU simulated annealing, a D-Wave QPU, or the Quip network. xqsa.build_solver(name, seed=...) selects a backend by name regardless of which surface above produced the model, and every example runner exposes this as a --solver flag. See Backends.

Quadratic Models

Every backend XQuad targets agrees on one thing: a single number, computed from a candidate assignment, that the solver tries to make as small as possible. That number is the model’s energy, and a quadratic model is the recipe for computing it:

$$H(x) = \sum_i \text{linear}[i] \cdot x_i + \sum_{i \le j} \text{quadratic}[i,j] \cdot x_i \cdot x_j$$

x is a vector of variables. linear[i] scales with variable i’s own value, and quadratic[i,j] scales with the product of i and j’s values – a cost, or a saving if negative. Read those as “the cost of turning i on” and “the extra cost of turning i and j on together” when x is binary; Three Domains below covers two domains where nothing is “on.” No term touches three variables at once – that is what “quadratic” means here. The sum runs over \(i \le j\), not \(i < j\): the diagonal is legal, and quadratic[i,i] scales with \(x_i^2\). What that means depends on the domain – on binary variables \(x^2 = x\), so it acts as a linear bias written through the quadratic table; on spin variables \(x^2 = 1\), so it is a constant offset. The VM stores and evaluates such a term without interpreting it. H is called the model’s Hamiltonian, borrowing the term physicists use for a system’s total energy, because the annealing hardware XQuad can target literally is a physical system settling toward a low-energy state.

XQVM builds this structure directly: BQMX/SQMX/XQMX allocate a model register, and SETLINE/SETQUAD/ADDLINE/ADDQUAD write the two coefficient maps one term at a time. See Allocators and Coefficient Access for the instructions.

Why Optimisation Problems Become One of These

A combinatorial problem – which cities to visit in what order, or which items fit in the knapsack – usually starts life as a set of decisions plus rules those decisions must obey. A quadratic model has no separate mechanism for rules: it is one function, and a solver’s only operation is minimising it. Every constraint has to become part of the same H, or the solver never sees it. The Penalties section below is that mechanism, and the concept this page most wants you to leave with.

A Worked Example: Max-Cut

Take Max-Cut: split a graph’s nodes into two groups to maximise the total weight of edges that cross between them. \(x_i \in \{0, 1\}\) marks which group node \(i\) is in. For each weighted edge \((i, j, w)\):

$$\text{linear}[i] \mathrel{+}= -w, \qquad \text{linear}[j] \mathrel{+}= -w, \qquad \text{quadratic}[i,j] \mathrel{+}= 2w$$

That edge’s contribution to \(H\) is \(-w(x_i + x_j - 2x_ix_j)\), and \(x_i + x_j - 2x_ix_j\) is the binary XOR of \(x_i\) and \(x_j\): it is \(1\) when \(x_i \neq x_j\) and \(0\) when they match. That edge contributes \(-w\) exactly when it crosses the partition, and \(0\) otherwise. Summed over every edge, \(H\) is the negative of the cut weight, so minimising \(H\) maximises the cut. examples/maxcut/runner.py builds exactly this model.

For three nodes and edges \((0,1,3)\), \((1,2,5)\), \((0,2,1)\), the rule above gives:

$$\text{linear} = \{0: -4,\ 1: -8,\ 2: -6\}, \qquad \text{quadratic} = \{(0,1): 6,\ (1,2): 10,\ (0,2): 2\}$$

Two assignments, worked by hand:

  • \(x = (1, 1, 0)\): \(H = (-4)(1) + (-8)(1) + (-6)(0) + 6(1)(1) + 10(1)(0) + 2(1)(0) = -12 + 6 = -6\). Nodes 0 and 1 share a group, so only the two edges crossing to node 2 count: \(5 + 1 = 6\), and \(-6\) is the negative of that, matching.
  • \(x = (1, 0, 1)\): \(H = (-4)(1) + (-8)(0) + (-6)(1) + 6(1)(0) + 10(0)(1) + 2(1)(1) = -10 + 2 = -8\). Every edge except \((0,2)\) crosses: \(3 + 5 = 8\), matching. This is the lowest of the four distinct cuts – a partition and its complement always have the same energy, so the eight assignments give four cuts – so it is the Max-Cut optimum for this graph.

Three Domains

x_i is not always a 0/1 bit. XQVM supports three domains, and the choice affects one thing above all: whether a backend can solve the result.

DomainValuesModel allocatorSample allocator
Binary\(\{0, 1\}\)BQMXBSMX
Spin\(\{-1, 1\}\)SQMXSSMX
Integer(\(k\))\(\{0, \ldots, k{-}1\}\), \(k \ge 2\)XQMXXSMX

Binary is QUBO – Quadratic Unconstrained Binary Optimisation – the domain most combinatorial formulations target directly: a variable is either selected or not. The Max-Cut model above is binary.

Spin is the Ising model: each variable is a magnetic moment pointing up or down, \(-1\) or \(+1\), which is the domain quantum annealing hardware minimises natively. A spin variable’s linear coefficient rewards one orientation and penalises the other by the same amount, rather than switching a cost on or off. Binary and spin describe the same choices: substituting \(x_i = (s_i + 1)/2\) (or \(s_i = 2x_i - 1\)) turns any binary Hamiltonian into a spin Hamiltonian over the same variables, with rescaled coefficients and one additive constant that does not change which assignment is optimal. Build a model in one domain and you can rebuild it in the other; between these two the choice is free, since every current xqsa solver accepts both, so pick whichever domain the problem is natural in.

Integer generalises past two states to give a variable \(k\) integer values directly, suited to a quantity with a natural ordering or magnitude – a position in a short list – without one-hot encoding it into several binary variables first. Unlike binary and spin, it is not a relabelling of the other two: it cannot encode an unordered categorical choice, because a quadratic form over integer variables cannot express that two values merely differ without also expressing by how much.

Integer is the domain with a real consequence: every current xqsa solver rejects it. An integer XqmxModel is a real thing you can build in XQVM bytecode today, but there is no backend yet that can solve one. See Backends.

A model and a sample share a domain and a variable count, but not a shape: a model is two sparse coefficient maps, linear and quadratic, while a sample is one value per variable and nothing else. Allocators covers both families and their default values in full.

Energy

Given a model and a candidate assignment (a sample), ENERGY computes \(H(x)\) for that specific x and pushes the result – the same formula as the model’s Hamiltonian above, evaluated at one point. “Energy” and “Hamiltonian value at x” mean the same thing throughout XQuad’s documentation. A solver’s job is to search for the x that minimises this value; XQVM’s job is only to compute it, which is what lets a program check a solver’s answer independently rather than trust it. See Energy Evaluation.

Penalties: Folding a Constraint into the Objective

The constraint instructions below expand assuming binary variables, where \(x^2 = x\). That identity does not hold for spin variables, where \(x^2 = 1\) always: apply these instructions to a BQMX model, and convert a spin model to binary first if you need one of them (see Three Domains).

Take the simplest useful constraint: exactly one of a set of variables should be 1 (a one-hot choice – pick exactly one city to visit first, one colour for this node). Written as a penalty term:

$$H \mathrel{+}= P \cdot \left(\sum_i x_i - 1\right)^2$$

The squared term is zero exactly when the constraint holds (the sum is 1) and strictly positive for every assignment that violates it – pick zero variables and the sum is 0, pick two and the sum is 2, either way \((\sum x_i - 1)^2 \ge 1\). Adding this to \(H\) means violating the constraint always costs at least \(P\) units of energy on top of whatever the rest of the objective says. A solver minimising the combined \(H\) has a direct incentive to satisfy the constraint, without any code path in the solver that knows constraints exist – it is minimising one number, the same way it always does.

This is the general shape behind most of XQVM’s high-level constraint instructions: ONEHOTR/ONEHOTC are exactly the sum above over a grid row or column, and EXCLUDE, IMPLIES, EQUALITY, ATLEAST, and ATLEASTW are the same idea applied to different rules: mutual exclusion, implication, weighted equality, at-least-k with unit weights, and at-least-k with arbitrary weights. The instruction reference also documents REDUCE, which uses the same technique – a term that is zero exactly when a condition holds – but to enforce an algebraic identity between variables rather than a rule from the problem; that is a different enough job to read about on its own. The full expansion for each instruction – which linear and quadratic coefficients change by how much – is reference material, not a concept, and lives on High-Level Constraints and in the normative HLF specification. This page is not repeating that table; the mechanism above is what to carry forward from it.

Choosing a Penalty Weight

\(P\) is not a detail to default and forget – it is the real engineering decision in penalty-based modelling, and it fails in both directions.

Too low, and the solver buys its way out of the constraint: \(P\) must exceed the largest objective improvement any violating assignment can buy over the best feasible one, or the minimiser takes that trade, because nothing in \(H\) told it not to. Suppose the Max-Cut model above also carried a one-hot constraint over nodes 0 and 2 only, with weight \(P\). The unconstrained optimum \(x = (1, 0, 1)\) worked out above violates it; the best assignments that satisfy it are \(x = (1, 1, 0)\), worked out above, and \(x = (0, 0, 1)\), both at \(H = -6\). Violating the constraint lowers the energy from \(-6\) to \(-8\), an improvement of \(2\), so penalty = 1 costs less than it saves and the minimiser still prefers the infeasible optimum. penalty = 2 ties the two, and \(P\) must exceed \(2\) before satisfying the constraint wins outright.

Too high does not corrupt the model itself. For any two assignments that both satisfy the constraint, the penalty term is 0 on both, so their energy difference is exactly the objective’s difference, independent of \(P\); ENERGY computes that difference with exact i64 arithmetic, so nothing in \(H\) degrades. What a large \(P\) costs happens outside the model. On a fixed-precision device, an objective difference much smaller than \(P\) can fall below the device’s resolution once the combined \(H\) is scaled to fit it, so the device stops representing that difference at all. And a large \(P\) raises the energy barrier between feasible regions, so a heuristic such as simulated annealing gets trapped in whichever feasible region it reaches first and stops exploring for a better one elsewhere.

Getting this right in practice – how to size \(P\) relative to your objective, and how to tell from a solver’s output which failure mode you hit – belongs to Constraints. What to take from this page is that the weight is a parameter you choose, not one XQVM chooses for you: every constraint instruction pops penalty off the stack as an ordinary operand.

Three Programs

An optimisation problem in XQuad is three independent programs sharing one instruction set: an encoder, a verifier, and a decoder. They communicate only through calldata in and outputs out. xqvm’s Three-Program Architecture defines this shape, so it is not something xqcp adds on top. Nothing in the instruction set enforces it – see Beyond xqcp – but every tool in the toolchain assumes it.

xqcp generates all three for you from one Problem definition: problem.compile() returns CompiledPrograms(encoder, verifier, decoder), three independent .xqasm sources, each its own complete program with its own fixed inputs and outputs. A hand-written program can follow the same shape with no DSL involved: xqvm/examples/tsp/main.rs builds and runs a three-program TSP pipeline directly against the Rust xqvm crate.

Why Three, Not One

XQVM has no instruction that calls a solver. Its 93 instructions cover control flow, stack and register I/O, arithmetic, comparison, vectors, grid operations, the constraint and energy family – nothing that reaches outside the VM to an annealer or a sampler. Solving happens in host code, between two runs of the VM, using whichever backend Backends describes. A model-building program cannot also be the program that checks and decodes the answer, because the answer does not exist yet when that program halts.

flowchart LR
    Q[Problem definition] -->|compile| ENC[Encoder]
    Q -->|compile| VER[Verifier]
    Q -->|compile| DEC[Decoder]
    ENC -->|XQMX model| H[Host program]
    H -->|model| SLV[xqsa solver]
    SLV -->|XQMX sample| H
    H -->|model, sample| VER
    H -->|sample| DEC
    VER -->|energy, valid| H
    DEC -->|decoded result| H

What Each Program Does

Encoder. Reads the problem’s runtime inputs from calldata, allocates the XQMX model, and emits every objective term and constraint penalty the DSL recorded. Its one output is the model, on slot 0. This is “a program whose job is to construct a model” – running it does not solve anything, it only builds the thing a solver will minimise.

Verifier. Takes the encoder’s own inputs, then the model and the sample, and checks whether the sample satisfies every constraint the encoder applied, then computes the sample’s energy with the ENERGY opcode. It outputs (energy, valid). It needs the encoder’s inputs because it replays the encoder to rebuild the constraint data, which lives in registers at VM runtime rather than in the model. This is how a sample’s feasibility gets checked independently of whatever backend produced it.

Decoder. Takes a sample and N, and extracts the answer in the problem’s own terms – a tour, a partition, a set of selected items – into one or more output vectors. It does not know or care whether the sample it was given is valid; that is the verifier’s job, not the decoder’s.

Independent, Not Sequential

The three programs share no state. Communication between them happens only through calldata in and outputs out – there is no hidden channel, and no program reads another program’s internals. The verifier and the decoder are not a pipeline: both read the same sample straight from the solver, at the same point, side by side. A host program can decode a sample the verifier just rejected, which is useful for inspecting what a bad solution actually looks like.

A Concrete Run

examples/maxcut/runner.py is exactly this shape. Max-Cut declares no constraints, so its verifier has none to check: the loop under ; === Validity checks === in the compiled verifier only confirms each sample value is 0 or 1. The ENERGY recomputation still runs and is real independent verification – the energy the verifier reports is computed fresh from the model and the sample, not relayed from the solver.

Its run() function makes four calls in sequence, three of them through the VM:

  1. vm.run(programs.encoder) with calldata [n, flat_edges] and one output slot, producing an XQMX model.
  2. solver.solve(model), entirely outside the VM, producing a sample.
  3. vm.run(programs.verifier) with calldata [n, flat_edges, model, sample] and two output slots, producing (energy, valid).
  4. vm.run(programs.decoder) with calldata [sample, n] and one output slot, producing the decoded partition.

Running uv run python examples/maxcut/runner.py --n 5 --seed 42 prints:

$ uv run python examples/maxcut/runner.py --n 5 --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "cut_weight": 354,
  "energy": -354,
  "n": 5,
  "partition": [
    0,
    1,
    0,
    0,
    1
  ],
  "valid": 1
}

Adding --interpreter rust to the same command prints that block byte for byte for this seed. That is not a guarantee: the encoder, verifier, and decoder are deterministic per interpreter – same bytecode in, same output out, on either one – but the solve sitting between them is only pinned to the seed and the dwave-samplers version, since SA is sensitive to BQM construction order. A different version of that library can return a different valid sample for the same seed. See below for what make example-smoke actually checks instead of byte-for-byte parity.

_note and _seed are the runner’s own bookkeeping, not part of the result: _note’s value, "canonical CI golden", describes what the runner calls this invocation, not a guarantee that a test pins against it. energy is the negative of cut_weight because the encoder minimises -weight per crossing edge to make the objective function, matching the derivation in Quadratic Models. valid: 1 here only confirms every sample value is in {0, 1}, which is all this problem’s verifier checks. make example-smoke is what actually guards this example: it runs both interpreters and checks valid == 1, and does not compare cut_weight, energy, or partition between them. These numbers depend on the dwave-samplers version behind dwave-cpu; a different version can return a different valid sample with a different cut weight.

The three VM calls above, and the solve between them, are the host program driving all three compiled programs and the solver together – see Ways to Use XQuad for the surfaces that can play that role.

Beyond xqcp

A hand-assembled .xqasm program is free to read inputs, build a model, and produce output in one file, the way Toolchain Map’s minimal add.xqasm example does – the interpreter does not enforce the three-program split. The split earns its cost once an external solver sits in the loop and a sample needs independent checking, which is exactly when reaching for xqcp, or hand-writing the same three-program shape, starts to pay off. See Modelling: Compiling for how the compiler produces the three programs, and Running Programs and Solving Overview for executing each stage.

Backends

“Hardware-agnostic” means one concrete thing: build a model once, then choose where it solves, from five backends behind a single interface. Nothing about the model, the encoder, or the verifier changes with that choice.

The Solver Set

NameClassWhere it solvesInstall
dwave-cpu (default)SolverDWaveCPULocally, on the CPU: simulated annealingbase xqsa
dwave-qpuSolverDWaveQPUD-Wave Leap cloud: physical quantum annealing hardwarexqsa[dwave]
cuda-gpuSolverCudaGPULocally, on an NVIDIA GPU: simulated annealingxqsa[cuda]
metal-gpuSolverMetalGPULocally, on an Apple GPU: simulated annealing or block Gibbs samplingxqsa[metal]
quipSolverQuipThe Quip network: submitted to a decentralised compute market, solved by a miner you do not controlxqsa[quip]

build_solver(name, seed=...) constructs any of the five from this name, so a caller can stay backend-agnostic. seed reaches the three classical simulated-annealing backends and is ignored by dwave-qpu (physical hardware has no seed) and quip (env-configured, no local randomness).

What Actually Distinguishes Them

Where the computation happens. dwave-cpu, cuda-gpu, and metal-gpu all run the identical algorithm family – simulated annealing – just on different local processors; picking between them is a performance question, not a correctness one. dwave-qpu hands the problem to real annealing hardware over the network. quip does not solve anything itself: it posts the model as a job to the network’s mempool, a miner solves it on hardware of its own choosing, and the adapter decodes whatever comes back.

Domain support. Every backend above accepts a binary or spin model and rejects anything else: Solver._validate_model() raises ValueError for any domain other than BINARY or SPIN. XQVM’s third domain, integer (XQMX), has no backend yet. See Quadratic Models for what the three domains are.

The result contract is identical regardless of backend. Every solve() call returns a SolverResult(sample, energy, timing, metadata). energy is not simply relayed from the device: XQuad’s own verifier program recomputes it independently with the ENERGY opcode, so you never have to take a backend’s word for its own energy – run the verifier and recompute it. See Three Programs.

Before You Choose

dwave-cpu needs no hardware and no credentials, so it is the default: the reproducible baseline every other backend is compared against. The other four need something this book cannot check for you – a GPU for cuda-gpu or metal-gpu, D-Wave Leap credentials for dwave-qpu, network configuration for quip – and setting each of those up belongs to the pages below.

Depth Lives in Solving

This page is orientation, not a manual. Backend-specific setup, parameter tuning, and result interpretation belong to Solving Overview, Local Solvers, D-Wave QPU, Quip Network, and Energy and Precision.

Modelling Lifecycle

XQCP is a Python-embedded DSL that turns a problem description into three XQVM assembly programs. You declare a Problem’s runtime inputs, allocate the quadratic model those inputs fill, add objective terms and constraints, declare what you want back, then call compile(). XQCP records every call you make against Problem as you make it and only turns that recording into .xqasm when compile() runs – see Compiling for how the recording becomes three programs.

A problem definition follows one call sequence, in order:

Problem(name) -> input()* -> define_model() -> body* -> output()* -> compile()

input() calls come first: Inputs and Model Shape covers why, and what a declared input becomes at run time. define_model() runs exactly once and makes problem.model available for the rest of the body: objective terms, constraints, and loops, in any order, covered across Expressions, Objectives, Constraints, and Control Flow. Your first output() call marks the start of the decoder section, covered in Outputs and Decoding.

Why XQCP, Not Assembly

Ways to Use XQuad names six surfaces for getting a problem into XQuad; XQCP is one of them. Reach for it when you are modelling a combinatorial problem from scratch and would otherwise hand-write the same loop-over-variables, add-a-coefficient pattern in .xqasm three times over, once per program – Problem records that pattern once, and the compiler emits all three. Write .xqasm directly instead when a program is small enough that the three-program duplication costs nothing to write by hand. Reach for xqvm::InstructionBuilder instead when a Rust host is generating programs and adding a Python step to emit them buys nothing.

One Problem, Once

Every page in this chapter builds on the same running example: examples/knapsack/runner.py. It takes item weights, item values, and a capacity, and chooses the subset of items maximising total value without exceeding the capacity. build_problem is the whole of it:

def build_problem(n: int, weights: list[int], values: list[int], capacity: int) -> Problem:
    problem = Problem("Knapsack")

    num_items = problem.input("num_items", type=Types.Int)
    weights_in = problem.input("weights", type=Types.Vec)
    values_in = problem.input("values", type=Types.Vec)
    capacity_in = problem.input("capacity", type=Types.Int)

    problem.define_model(size=num_items, domain=Domain.BINARY)

    # Objective: minimise -sum(v_i * x_i)
    with problem.range(0, num_items) as i:
        vi = problem.stow("vi", values_in.get(i))
        problem.model.linear[i].add(-vi)

    # Constraint: sum(w_i * x_i) <= W
    indices = problem.vec()
    coeffs = problem.vec()
    with problem.range(0, num_items) as i:
        indices.push(i)
        coeffs.push(weights_in.get(i))

    problem.slack(indices, coeffs, num_items, capacity_in)
    problem.model.apply_equality(indices, coeffs, capacity_in, 100)

    selected = problem.output("selected", type=Types.Vec)
    with problem.range(0, num_items) as i:
        selected.append(problem.sample.getline(i))

    return problem

One binary variable per item: x_i = 1 means item i is selected. Four inputs, one 1D binary model sized to num_items, a range loop building the objective, a second range loop paired with slack and apply_equality encoding the capacity constraint, and a decoder loop reading the solved sample back into a selected vector. Running it end to end:

$ uv run python examples/knapsack/runner.py --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "capacity": 18,
  "energy": -32446,
  "n": 5,
  "selection": [
    1,
    1,
    1,
    0,
    1
  ],
  "total_value": 46,
  "total_weight": 12,
  "valid": 1,
  "values": [
    5,
    4,
    18,
    3,
    19
  ],
  "weights": [
    2,
    1,
    5,
    4,
    4
  ]
}

_note and _seed are the runner’s own bookkeeping, the same as in Three Programs. energy is the full model’s Hamiltonian, capacity constraint included, so it is not simply -total_value here the way Max-Cut’s energy is -cut_weightObjectives isolates just the objective term’s contribution, and Constraints covers what the rest of energy is paying for.

This run is not the optimum: the five weights sum to 16, under the capacity of 18, so every item fits and the true best value is 49, not the 46 printed above. A heuristic solver returns a good sample, not a proven best one. valid: 1 reports only the verifier’s binary-domain check here, not the capacity constraint; see Compiling for what a verifier actually checks.

Inputs and Model Shape starts with the four problem.input() calls and the define_model() line above; the rest of the chapter works through the remaining lines in the order they appear.

Where to Go From Here

  • Inputs and Model Shape – runtime inputs and the 1D/2D model they fill.
  • Expressions – the operators and functions that build a coefficient or a loop bound.
  • Objectives – turning “minimise this” into linear/quadratic coefficients, sign included.
  • Constraints – folding a rule into the objective as a penalty.
  • Control Flowrange, iter, and branch.
  • Outputs and Decoding – reading a solved sample back into your problem’s own terms.
  • Compiling – what problem.compile() actually produces.

Inputs and Model Shape

A Problem definition starts with two declarations: the runtime inputs it reads, and the model those inputs fill. Both happen before anything else – spec/xqcp/SPEC.md fixes the order as Problem(name) -> input()* -> define_model() -> body* -> output()* -> compile(), and input() calls after define_model() raise RuntimeError.

Every code sample in this chapter, and the rest of Part III, assumes the same import:

from xquad.cp import Domain, Problem, Types

It is shown once, here, and omitted everywhere after.

Inputs

problem.input(name, type=Types.Int) or type=Types.Vec declares one runtime value and returns an InputRef you use for the rest of the problem body. examples/knapsack/runner.py declares four:

num_items = problem.input("num_items", type=Types.Int)
weights_in = problem.input("weights", type=Types.Vec)
values_in = problem.input("values", type=Types.Vec)
capacity_in = problem.input("capacity", type=Types.Int)

Types.Int is a scalar integer, and Types.Vec is a vector of integers. Every runtime input is one of these two types.

Declaration Order Is Calldata Order

Compiling the fragment above emits this for each input:

PUSH 0
INPUT r0
PUSH 1
INPUT r1

INPUT pops a calldata slot index and clones calldata[slot] into the target register. It does not check the calldata value’s type against anything (Register I/O covers INPUT in full). XQCP’s register allocator hands out registers in call order starting at r0, and because input() must run before define_model(), the first input() call is allocated r0, the second r1, each next call the next register, with nothing else allocated between them. That is why the slot index XQCP pushes before each INPUT matches the input’s position in your problem.input() call sequence: your Nth input() call reads calldata[N-1].

The host has to supply calldata in that same order. examples/knapsack/runner.py calls vm.set_calldata([n, weights, values, capacity]), matching num_items, weights_in, values_in, capacity_in above one for one. Nothing checks this correspondence: INPUT clones whatever sits at that slot, regardless of the input’s declared type. Swap the position of weights and values on either side and both are Types.Vec, so no RegisterType fault stops you – the encoder runs to completion and reads weights where it wanted values. See Calldata and Outputs for how a host supplies calldata to any XQVM program, XQCP-generated or hand-written.

Reading a Vec Input

A Types.Vec input supports two operations no Types.Int input has:

values_in.get(i)      # VECGET: element at index i
values_in.veclen()    # VECLEN: the vector's length

Both return an expression, not a value – see Expressions for what you can build with the result. Calling either on a Types.Int input raises TypeError immediately, at problem-definition time, not at compile time:

>>> num_items.get(0)
TypeError: Cannot index into int input 'num_items'

The Model

problem.define_model(size, domain, rows=None, cols=None) allocates the quadratic model the encoder builds and runs exactly once per problem. Before it runs, problem.model and problem.sample raise RuntimeError; after, both are available for the rest of the chapter. size is the total variable count, as an int or an expression built from your inputs – knapsack sizes its model directly off an input:

problem.define_model(size=num_items, domain=Domain.BINARY)

domain is a Domain member; see Quadratic Models for what each domain means and how to choose between them, since that choice does not belong to this page. Binary is the domain the running examples in this book use. Domain also accepts the XQMXDomain it wraps, so code written against the VM enum keeps working.

Integer Variables

An integer variable takes one of k values rather than two. Give the width directly:

problem.define_model(size=num_assets, domain=Domain.INTEGER, k=4)

That allocates num_assets variables over {0, 1, 2, 3}. k is an expression like size, so it may come from calldata.

Where the values you are modelling are not zero-based, give bounds instead:

problem.define_model(size=num_assets, domain=Domain.INTEGER, lo=-5, hi=5)

You then write coefficients over x in [-5, 5] while the model holds y = x - lo in {0, ..., 10}, and sample.value(i) shifts back on the way out. XQCP rewrites each quadratic write for you: w * x_i * x_j expands to w*y_i*y_j + w*lo*y_i + w*lo*y_j + w*lo^2 once x = y + lo is substituted, so the write records w*lo against the linear coefficient of both indices as well. The w*lo^2 constant is dropped, because XQMX has no offset field. Energies shift by the same amount for every assignment, so the minimum is still in the same place; the number is not the objective’s true value. Linear writes need no correction.

Two consequences worth knowing before you reach for the ranged form. Setting a coefficient is refused on it, on quadratic[i, j] = w and linear[i] = w alike, because setting replaces a coefficient while the corrections can only accumulate: two writes to the same pair would disagree, and a linear set would drop whatever corrections earlier quadratic writes had left on that index. Use .add(), which loses nothing, since a coefficient starts at zero. And a runtime lo competes with an output loop bound for the decoder’s single calldata scalar, so compile() raises naming both. Literal bounds sidestep it.

Constraints are binary-only. On a spin or integer model every apply_* method is refused and only coefficient writes are supported, because each expansion in the VM is derived under x^2 = x. Write the penalty out by hand, as Portfolio Rebalance does for its budget.

Categorical Variables

A categorical variable takes one of k unordered cases. There is no VM domain for that, so XQCP records the standard encoding:

problem.define_model(size=num_nodes, domain=Domain.CATEGORICAL, k=num_colors, penalty=200)

That is a num_nodes x num_colors binary grid with one ONEHOTR per row, built through the same calls you would have written yourself. The model is binary afterwards, so constraints work on it as usual and coefficient access is (variable, case). Read the answer back with sample.case(v), which gives the case a variable took or -1 if its row came back empty.

1D and 2D Models

A model is either flat (1D, indexed 0..size) or a grid (2D, indexed by (row, col)). Every model in this chapter is 1D: knapsack’s x_i is one binary decision per item, with no row/column structure to it. Pass rows and cols to get a grid instead:

problem.define_model(size=rows * cols, domain=Domain.BINARY, rows=rows, cols=cols)

rows and cols are both-or-neither: define_model() raises ValueError when given exactly one, rather than building a 1D model that fails later.

Once a model has a shape, coefficient access accepts a (row, col) tuple in place of a flat index, and XQCP flattens it for you. Compiling

n = problem.input("n", type=Types.Int)
problem.define_model(size=n * n, domain=Domain.BINARY, rows=n, cols=n)
problem.model.linear[(1, 2)] = 99

emits the block below, once n has claimed r0 and define_model() has claimed r1 for cols and r2 for the model:

PUSH 1
PUSH 2
LOAD r1
IDXGRID
PUSH 99
SETLINE r2

r1 is the cols register define_model() allocated; this block does not depend on n’s runtime value at all, only on how many calls came before it. IDXGRID computes row * cols + col, so at run time, with n = 3, the coordinate (1, 2) flattens to 1 * 3 + 2 = 5, and every coordinate-accepting call on a 2D model (coefficient access, apply_exclude, apply_implies) goes through the same flattening. size still has to equal rows * cols yourself; XQCP does not derive one from the other. It does optimise the size expression itself: size=n*n emits a single SQR instead of LOAD, LOAD, MUL whenever both multiplicands are the same register, 2D model or not. Grid allocation itself – RESIZE and the rest of what BQMX r2 plus a shape actually builds – is Allocators and Grid Operations territory, not this page’s.

Symbolic Reference Types Introduced Here

spec/xqcp/TYPES.md is the normative reference for every symbolic type XQCP hands back. The two this page introduces:

TypeCreated byRegister type
InputRefproblem.input(name, type)int or vec
ModelRefproblem.define_model(...)xqmx

SampleRefproblem.sample, the model’s read-only counterpart used in the decoder – is created together with ModelRef but belongs to Outputs and Decoding, where it is actually used. Expressions covers LoopVar and RegLoad, the two symbolic types this page’s fragments do not need yet; the full type table, including register-allocation and error-condition details past what this page restates, is in spec/xqcp/TYPES.md.

With inputs declared and a model allocated, the next thing every problem body needs is a way to compute with them – see Expressions.

Expressions

Every symbolic value XQCP hands you – an InputRef from problem.input(), a LoopVar from problem.range()/problem.iter(), a RegLoad from problem.stow(), a coefficient read back from model.linear[i] – builds the same kind of expression tree when you combine it with an operator or an xq_* function. A plain Python int used anywhere one of these is expected gets coerced to a literal automatically; anything else raises TypeError. This page groups the resulting vocabulary by what you are trying to compute, not by class hierarchy.

Every xq_* function below imports from the same module as Problem and Types:

from xquad.cp import (
    xq_not, xq_and, xq_or, xq_xor, xq_bnot,
    xq_sqr, xq_abs, xq_min, xq_max, xq_bitlen,
    xq_triu, xq_grid,
)

Arithmetic: Computing a Value

+ - * // % and unary - work exactly as Python’s operators do on integers, and compile to ADD SUB MUL DIV MOD NEG. Knapsack’s objective loop uses three of them in three lines:

with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)

-vi is unary negation on a RegLoad; problem.range and problem.iter yield LoopVars (i here) with the same arithmetic, used as loop-bound math in Max-Cut’s offset = e * 3. // is integer division, matching XQVM’s DIV, not Python’s floating-point / – XQCP has no /, since every value in this system is an i64.

Adding or subtracting exactly the literal 1 is special-cased: a + 1 compiles to LOAD, INC and a - 1 to LOAD, DEC, skipping the PUSH 1 a general add or subtract would need. Nothing about how you write it changes – a + 1 and a - 1 are the natural way to write these, and the compiler does the substitution for you.

Comparisons: Producing 0 or 1

== < > <= >= compile to EQ LT GT LTE GTE, each pushing 1 for true and 0 for false rather than a Python bool. That makes a comparison’s result usable anywhere an integer is: as a coefficient directly, as the condition argument to problem.branch() (see Control Flow), or combined further with arithmetic.

Python has no != here. There is no XQVM opcode for it, so != on an XQCP expression raises immediately, at problem-definition time:

TypeError: '!=' is not supported on XQCP expressions; use xq_not(a == b) instead

_ExprOps overloads __ne__ purely to raise that. Left to Python’s default, __ne__ would call __eq__ and invert the result, and inverting a CompareOp object with not only asks whether the object itself is falsy – which it never is – so a != b would evaluate to the constant False regardless of a and b. Write xq_not(a == b) instead:

model.linear[0] = xq_not(a == b)

compiles to LOAD, LOAD, EQ, NOT, and gives 1 when a and b differ, 0 when they match.

Bitwise: Bit-Level Values

& | ^ ~ << >> compile to BAND BOR BXOR BNOT SHL SHR, useful wherever a problem packs several small values into one integer, or – as in knapsack’s slack step – builds a binary-weighted sum out of individual bits. These are ordinary Python operators, not xq_* functions, because Python lets you overload them.

Index Math: One Number From Several

Two functions turn a pair (or triple) of coordinates into a single flat index:

xq_triu(i, j)            # IDXTRIU: upper-triangular packed index for (i, j)
xq_grid(row, col, cols)  # IDXGRID: row * cols + col, grid flat index

xq_grid is what a 2D model’s tuple-coordinate coefficient access uses internally – see Inputs and Model Shape for that path; call it directly when you need the flat index as a value in its own right rather than as a coordinate to model.linear[...]. xq_triu(i, j) swaps i and j when i > j, then computes j * (j - 1) // 2 + i on the swapped pair, so the packed index does not depend on argument order, per the swap rule spec/xqvm/ISA.md gives for IDXTRIU. xq_triu(2, 5) returns 12 (5 * 4 // 2 + 2; no swap needed, since 2 <= 5).

Logical and Other Free Functions

Python’s and, or, and not keywords cannot be overloaded. XQCP gives you functions in their place. The first four work on the 0/1 convention comparisons use, not on Python truthiness:

xq_not(x)       # NOT:  0 -> 1, non-zero -> 0
xq_and(a, b)    # AND
xq_or(a, b)     # OR
xq_xor(a, b)    # XOR
xq_bnot(x)      # BNOT: bitwise complement, the same opcode as ~ above,
                # kept as a function for symmetry -- not the same as xq_not

Writing a and b, a or b, not a, or using an expression as an if condition instead of calling one of these functions raises immediately, the same way != does above:

TypeError: XQCP expressions cannot be used in a boolean context ('and', 'or', 'not', 'if'); use xq_and(a, b), xq_or(a, b) or xq_not(a) instead

Python’s and/or/not/if all decide on the truthiness of the object, not on the value it represents, so XQCP has no way to give them the right answer – only to refuse before a Python habit produces the wrong operand or the constant False in place of a real expression.

Five more free functions round out the arithmetic vocabulary that has no Python operator:

xq_sqr(x)       # SQR:  x * x
xq_abs(x)       # ABS:  absolute value
xq_min(a, b)    # MIN
xq_max(a, b)    # MAX
xq_bitlen(x)    # BITLEN: floor(log2(x)) + 1, and 0 for x <= 0

Naming an Expression: stow

problem.stow(name, expr) evaluates an expression once and stores it in a register, returning a RegLoad you can reuse without re-emitting the computation. Knapsack’s vi = problem.stow("vi", values_in.get(i)) is this: values_in.get(i) is a VECGET, emitted once per loop iteration and bound to vi, and every later use of vi in that iteration is a cheap LOAD instead of repeating the VECGET. Passing an existing RegLoad back into stow overwrites that register instead of allocating a new one – useful for an accumulator threaded through a loop.

The operators and free functions above are the DSL’s whole vocabulary; spec/xqcp/TYPES.md lists nothing this page omits. With that in hand, the next question is what you do with a value once you have one: Objectives covers turning an expression into a term the solver minimises.

Objectives

An objective is the part of a model’s Hamiltonian you write directly, as opposed to the part a constraint instruction adds on your behalf – see Quadratic Models for what the Hamiltonian \(H\) is and why a solver minimising it is the whole mechanism. In XQCP terms, an objective is whatever you assign or add to model.linear[i] and model.quadratic[i, j] before any constraint method runs.

Solvers Minimise. Say So.

Every backend XQuad targets minimises. If your problem is a maximisation, you have to negate it, and getting this backwards produces a model that runs, verifies, and confidently returns the worst answer instead of the best one – nothing checks that you meant to maximise.

Knapsack maximises total value. examples/knapsack/runner.py writes the objective as:

# Objective: minimise -sum(v_i * x_i)
with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)

x_i = 1 means item i is selected, and -vi is the sign flip: instead of rewarding selection with +v_i, the objective penalises it with -v_i, so minimising H picks large values, not small ones. Isolating just this loop – no capacity constraint – and running the encoder with num_items=5, values=[5, 4, 18, 3, 19], then computing ENERGY against the sample [1, 1, 1, 0, 1] (items 0, 1, 2, 4 selected, values 5 + 4 + 18 + 19 = 46) gives:

objective-only energy: -46
expected: -(sum of selected values) = -46

Energy is exactly the negative of total value, for any selection, because that is what linear[i] = -v_i means: drop the - and the same solver would minimise total value instead, selecting nothing.

Assign or Accumulate

model.linear[i] and model.quadratic[i, j] are CoefficientRef proxies, not values. Three things you can do with one:

SyntaxOpcodeEffect
model.linear[i] = wSETLINECoefficient becomes w, replacing whatever was there
model.linear[i].add(w)ADDLINECoefficient becomes current + w
x = model.linear[i]GETLINERead the current coefficient as an expression

The same three exist for model.quadratic[i, j], via SETQUAD/ADDQUAD/ GETQUAD. The distinction matters the moment more than one term touches the same coefficient – knapsack’s .add(-vi) runs once per item, each touching a different linear[i], so assign and accumulate would agree there. They stop agreeing as soon as two terms share a coefficient: two .add() calls against the same linear[0] with weights 5 and 7 leave it at 12; two = assignments with the same weights leave it at 7, the second overwriting the first outright. Reach for .add() any time a coefficient might accumulate contributions from more than one place, which for an objective built inside a loop – the usual shape – is the common case; reach for = when you know this is the coefficient’s only source.

Quadratic Terms

A quadratic coefficient couples two variables, and the same accumulate pattern applies. Max-Cut’s objective is the DSL rendition of the Max-Cut coefficient rule Quadratic Models derives by hand – for each weighted edge (i, j, w):

problem.model.linear[i].add(-w)
problem.model.linear[j].add(-w)
problem.model.quadratic[i, j].add(w * 2)

three .add() calls, one per term of \(\text{linear}[i] \mathrel{+}= -w,\ \text{linear}[j] \mathrel{+}= -w,\ \text{quadratic}[i,j] \mathrel{+}= 2w\). .add() is what makes this loop correct: a node touched by several edges accumulates a -w contribution from each one, and = would let the last edge silently overwrite every edge before it. Running examples/maxcut/runner.py --n 5 --seed 42 confirms the sign on the result – energy: -354 against cut_weight: 354 – matching the same “minimise the negative” pattern knapsack uses, for the same reason.

Coefficient access on a 2D model accepts (row, col) tuples in place of flat indices, for both linear and quadratic coordinates independently – see Inputs and Model Shape for the flattening XQCP applies; nothing about objective assignment changes once a coordinate is a tuple instead of an int.

Beyond Quadratic: model.reduce()

Everything above stops at degree two: one or two variables per term. model.reduce(var_a, var_b, p_aux) -> RegLoad is the only way past that. It performs a Rosenberg degree reduction: allocate one fresh auxiliary variable w at model.size, grow model.size by one, and add enforcement terms so w behaves as var_a AND var_b: p_aux * (x_a*x_b - 2*x_a*w - 2*x_b*w + 3*w), which is 0 when w == x_a AND x_b and strictly positive otherwise. It returns w as a RegLoad, an ordinary variable index you can use in a further quadratic term, or feed into a second reduce() call to reach one degree higher still:

w = problem.model.reduce(ti, tj, p_aux)
problem.model.quadratic[w, tk].add(coeff)   # coeff * x_i * x_j * x_k

examples/cubic_opt/runner.py builds a cubic term this way, one reduce() call per term inside a problem.range() loop; max3sat and portfolio_opt use the same one-call pattern for their own cubic terms. examples/quartic_opt/runner.py chains two reduce() calls to reach a quartic term – the reason reduce() returns a RegLoad rather than nothing.

p_aux is a penalty in its own right, separate from any constraint penalty in the same model: it has to be large enough that violating the w == x_a AND x_b relationship is never worth it, the same reasoning Constraints applies to constraint penalties. Each reduce() call that actually executes grows model.size by one, so the final variable count is size plus the number of reduce() calls executed – loops included. cubic_opt allocates one auxiliary variable per cubic term inside its loop, not one for the whole model.

reduce() hangs off model, like every constraint form, but it is not one: XQCP keeps it out of the DSL’s constraint bookkeeping deliberately, since it is a structural transformation of the model, not a domain rule about a solution. That is also why it lives on this page and not Constraints.

How Large an Objective Gets

An objective’s magnitude is bounded by what its coefficients can sum to. Knapsack’s linear coefficients are -values, so its objective ranges from 0 (nothing selected) down to -49 for values = [5, 4, 18, 3, 19] (everything selected, which for this instance fits the capacity: the weights [2, 1, 5, 4, 4] sum to 16, under the capacity of 18) – a range fixed entirely by the problem’s own numbers, before any constraint enters the picture. Constraints picks a penalty weight relative to a range like this one; this page stops at producing the range, not sizing anything against it.

With an objective in place, the next step most problems need is a rule the objective alone cannot express – see Constraints for folding one into the same Hamiltonian.

Constraints

A constraint is a rule a solution must obey. XQCP has no separate mechanism for rules – every constraint call turns into a penalty term added to the model’s Hamiltonian, the same H Quadratic Models describes. This page covers the constraint forms the DSL exposes, when to reach for each, and how to size the penalty weight in practice. For the linear/quadratic coefficient deltas each opcode produces, see High-Level Constraints; this page does not repeat that table.

Every constraint method hangs off problem.model, the ModelRef that define_model() makes available, and every one takes a penalty argument.

The Constraint Forms

DSL callEnforcesReach for it when
model.apply_onehot_row(row, penalty)Exactly one variable in grid row row is 1A 2D model needs “exactly one choice per row” – one city per tour position. Requires a grid; row must name one the grid declares
model.apply_onehot_col(col, penalty)Exactly one variable in grid column col is 1The column-wise mirror – one position per city. Same grid requirement
model.apply_exclude(a, b, penalty)a and b are not both 1Two choices conflict and picking both is meaningless or invalid
model.apply_implies(a, b, penalty)If a is 1, b is 1One choice requires another – selecting a route requires its start node open
model.apply_equality(indices, coeffs, target, penalty)sum(coeffs[k] * x[indices[k]]) == targetA weighted sum equals an exact value – one-hot is a special case of this (all-1 coefficients, target 1); exclude is not, despite the resemblance
model.apply_atleast(indices, k, penalty)At least k of indices are 1A minimum count, unweighted – cover at least k elements
model.apply_atleastw(indices, coeffs, k, penalty)sum(coeffs[j] * x[indices[j]]) >= kA minimum weighted sum – cover at least k units of capacity
model.apply_inequality(indices, coeffs, target, capacity, penalty)sum(coeffs[k] * x[indices[k]]) <= capacityA capacity-style bound in one call – composes problem.slack() and apply_equality(), shown separately below. The third parameter is named target in the source but is not a target value: it is where slack variables begin, normally the count of real variables. capacity is the bound. Pass it positionally

onehot_row/onehot_col, exclude and implies take coordinates directly; on a 2D model those coordinates can be (row, col) tuples, flattened automatically. equality, atleast, atleastw and inequality instead take two vector registers built with problem.vec() and .push(): indices names which variables participate, coeffs (where present) weights them.

A minimal \(2 \times 2\) grid problem exercises the row/column/pairwise forms together:

problem.define_model(size=n * n, domain=Domain.BINARY, rows=n, cols=n)
with problem.range(0, n) as r:
    problem.model.apply_onehot_row(r, penalty=50)
with problem.range(0, n) as c:
    problem.model.apply_onehot_col(c, penalty=50)
problem.model.apply_exclude((0, 0), (1, 1), penalty=50)
problem.model.apply_implies((0, 1), (1, 0), penalty=50)

rows and cols on define_model are what make the two one-hot forms legal. Omit them and every apply_onehot_row/apply_onehot_col call raises InvalidGridDimensions at run time, on either interpreter – xquad verify cannot catch it, because grid extents are runtime values. The product rows * cols must also fit inside size; a grid cannot describe cells the model never declared. atleast, atleastw and inequality append slack variables past the grid and only ever grow size, so they never invalidate a grid that was legal when it was set.

Compiling and running this for n = 2 produces a model whose linear and quadratic maps match the row/column penalties plus the two pairwise terms:

linear:    {0: -100, 1: -50, 2: -100, 3: -100}
quadratic: {(0, 1): 100, (0, 2): 100, (0, 3): 50, (1, 2): -50, (1, 3): 100, (2, 3): 100}

atleast and atleastw allocate slack variables and grow model.size directly, as part of their own expansion – ATLEAST’s own expansion in HLF.md builds the slack terms itself rather than composing with the SLACK instruction below. That is a real difference, not an implementation detail: SLACK takes no model operand and only appends to the indices/coeffs vectors it is given, so it never touches model.size by itself. A problem.slack() call grows the model only later and indirectly, when a subsequent apply_equality() widens it to max(indices) + 1 – a problem.slack() result that apply_equality() never consumes leaves the model unchanged. Each atleast/atleastw call, by contrast, allocates floor(log2(max_excess)) + 1 slack variables, where max_excess is N - k for atleast and sum(coeffs) - k for atleastw, and zero slack variables when max_excess is zero or less. A 3-item apply_atleast(idx, 2, penalty=30) allocates one slack variable, since max_excess = 3 - 2 = 1. apply_atleastw(idx, coeffs, 3, penalty=30) on the same three indices then allocates zero more for coeffs = [1, 1, 1], one more for coeffs = [1, 1, 2], or two more for coeffs = [1, 2, 3].

Turning an Inequality into an Equality

None of the forms above is <= or >= on a plain sum – atleast/atleastw cover >=, but a capacity constraint like knapsack’s (“total weight at most W”) needs the other direction. problem.slack() bridges the gap: it appends binary-weighted slack variables to an indices/coeffs pair so that an EQUALITY constraint over the combined vector is satisfiable exactly when the original inequality holds. See SLACK for the bit-width formula.

examples/knapsack/runner.py builds its capacity constraint this way:

indices = problem.vec()
coeffs = problem.vec()
with problem.range(0, num_items) as i:
    indices.push(i)
    coeffs.push(weights_in.get(i))

problem.slack(indices, coeffs, num_items, capacity_in)
problem.model.apply_equality(indices, coeffs, capacity_in, 100)

indices/coeffs start as the item weights, one entry per item. slack appends slack variables starting at index num_items (the model’s current size) sized to absorb up to capacity_in of unused capacity. apply_equality then constrains sum(weight_i * x_i) + sum(slack bits) == capacity. If the items sum to less than capacity, some slack combination fills the gap exactly. If they sum to more, no slack combination can, because slack only adds. The compiled encoder shows the two instructions back to back:

LOAD r0
LOAD r3
SLACK r7 r8

LOAD r3
PUSH 0x64
EQUALITY r4 r7 r8

model.apply_inequality(indices, coeffs, num_items, capacity_in, 100) composes exactly these two calls into one and produces the same model. Reach for the two-call form when you need indices/coeffs for something else afterward, or want the two steps visible; reach for apply_inequality otherwise.

Why the Reported Energy Is Not Just -total_value

EQUALITY’s expansion adds penalty * a_k * (a_k - 2b) and 2 * penalty * a_k * a_m terms but drops the constant penalty * b^2, since it shifts every assignment’s energy equally and does not change which one is optimal – see HLF.md. Dropping a constant from the model does not drop it from ENERGY’s result: a feasible sample still carries -penalty * b^2 as a fixed offset, because the terms that remain evaluate to exactly that once the constraint holds.

Running examples/knapsack/runner.py --seed 42 selects items worth 46 in total against a capacity of 18, at penalty = 100, and reports energy = -32446. That is -46 - 100 * 18^2 = -46 - 32400 = -32446: the objective contribution and the constant the equality constraint leaves behind, added together. Objectives isolates the -46 on its own; this is where the other -32400 comes from.

Choosing a Penalty Weight

Quadratic Models gives a safe upper bound: penalty must exceed the largest objective improvement any violating assignment can buy over the best feasible one. It is not the tightest bound – an equality-shaped violation (equality, one-hot, atleast, atleastw) with integer excess d actually pays penalty * d^2, and exclude/implies pay penalty outright, the d = 1 case. The rest of this section finds that tighter threshold by accounting for d directly.

Enumerate the Failure, Not the Intuition

Take a 4-item knapsack: weights [3, 4, 5, 2], values [4, 5, 6, 3], capacity 7. The best feasible selection is items {0, 1} or {2, 3}, tied at value 9 and weight 7. Every selection that exceeds capacity is a candidate the solver might prefer instead, if the penalty is too cheap. Seven of the sixteen selections exceed it:

ItemsWeightValueExcess over capacityValue gap over 9Rejection threshold
{0, 2}810111.00
{1, 2}911220.50
{0, 1, 3}912230.75
{0, 2, 3}1013340.44
{1, 2, 3}1114450.31
{0, 1, 2}1215560.24
{0, 1, 2, 3}1418790.18

The last column is value gap / excess^2, because EQUALITY’s penalty term is penalty * (sum - target)^2: a selection exceeding capacity by d pays penalty * d^2, not penalty. The largest entry in that column, not the largest value gap, sets the threshold: {0, 2} needs penalty > 1, the largest threshold in the table, while {0, 1, 2, 3} – the biggest value gain – needs only penalty > 0.18, because its excess of 7 is squared away. Reasoning from the value gap alone (9, from the worst offender by value) is safe, but it oversizes the weight here: the raw gap suggests penalty > 9, nine times the penalty > 1 this problem actually needs.

Compiling this exact problem and brute-forcing every assignment of the real QUBO – 4 item variables plus the 3 slack bits SLACK allocates for a capacity of 7 – confirms the table: at penalty = 1, the true optimum is a three-way tie at H = -58 between the two feasible sets and the infeasible {0, 2}; at penalty = 2, only the two feasible sets remain, both at H = -107. penalty = 1 is the exact boundary the table predicts, not an approximation of it.

A tie at the boundary is not a tie a sampler breaks the same way every seed. Sampling this exact model at penalty = 1 with SolverDWaveCPU (200 reads, Rust backend) across eight seeds returns the infeasible {0, 2} on two of the eight, at the identical energy, -58, as every feasible seed; the other six split across the two feasible ties. A single feasible sample at a borderline weight is not evidence the weight is safe – the tie means either outcome is a true optimum, and only the rate across repeated solves, each with a different solver seed, separates a weight sitting on the boundary from one below it. A penalty that is actually below its threshold, rather than sitting on it, behaves differently: building a second 4-item instance (weights [5, 4, 2, 5], values [7, 4, 3, 9], capacity 6) whose tightest threshold works out to penalty > 3, the identical penalty = 1 is now strictly below it, and all eight seeds return the infeasible pick, every time, because there the infeasible assignment is the unique optimum rather than one member of a tie.

When You Cannot Enumerate

Most problems are too large to brute-force. Enumerate a small instance of the same shape if you can, the way the table above does, since the qualitative lesson – the tightest violator, not the most valuable one, sets the threshold – carries over. When even that is impractical, a safe but loose fallback exists for a purely linear objective: since each variable contributes to it at most once, no two full assignments can differ by more than the sum of the absolute values of every linear coefficient, and any nonzero integer excess is at least 1, so penalty greater than that sum always rejects every violation. A quadratic objective needs the same sum extended to include the absolute value of every quadratic coefficient too, since a quadratic[i,j] term can also flip between two assignments. For the table above that sum is 4 + 5 + 6 + 3 = 18, eighteen times the actual boundary of 1 – a real bound, not the largest single coefficient, and worth using only until you can afford to enumerate a representative case.

The seed-42 knapsack run printed in One Problem, Once has values [5, 4, 18, 3, 19], and knapsack’s objective is -values (see Objectives), so the same fallback sum is 5 + 4 + 18 + 3 + 19 = 49. The runner’s own penalty = 100 clears that sum outright, so it is safe by this loose bound alone, without needing the per-instance enumeration this page otherwise recommends.

Too High Does Not Corrupt the Model

Raising penalty from 2 to 1000 on the same problem leaves the gap between the best and second-best true optimum at exactly 1 in both cases, for the reason Quadratic Models gives: the penalty term is zero on every feasible assignment, so it cannot change their relative order.

What a large penalty costs instead happens outside the model, on the hardware that has to represent it – see Energy and Precision for that trade-off, and Selection Under Budget for a worked recipe built on this page.

Control Flow

XQCP gives you four constructs for anything beyond a flat sequence of calls: problem.range(), problem.iter(), problem.branch(), and problem.stow(). Each is a Python API you call while building a Problem, and each records something into the action list that compile() later turns into real XQVM instructions. That gap between the two is the first thing to get straight, because it decides what each construct actually buys you.

A Python Loop and a Compiled Loop Are Different Things

xqcp is a Python program that runs once, when you call problem.compile(), to produce an .xqasm string. An ordinary Python for loop inside your problem definition runs during that one recording pass – it does not exist in the output at all. Three plain calls to add(1) inside for i in range(3) record three separate add_linear actions, one per concrete index, and the compiled encoder shows exactly that: no loop instruction, three ADDLINE blocks back to back.

for i in range(3):
    problem.model.linear[i].add(1)
PUSH 0
PUSH 1
ADDLINE r1
PUSH 1
PUSH 1
ADDLINE r1
PUSH 2
PUSH 1
ADDLINE r1

problem.range() is different: it records one RANGE loop that the VM executes at run time, over a count that need not be known until calldata arrives. with problem.range(0, num_items) as i compiles to a single RANGE/NEXT pair regardless of how large num_items turns out to be, because num_items is a runtime input, not a Python integer the recorder can unroll:

with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)
PUSH 0
LOAD r0
RANGE
  LVAL r5
  LOAD r5
  VECGET r2
  STOW r6
  LOAD r5
  LOAD r6
  NEG
  ADDLINE r4
NEXT

This is examples/knapsack/runner.py’s objective loop, unaltered. Use a plain Python loop only when the count is fixed at problem-definition time and small; use problem.range() whenever the count depends on an input, which is the common case, since num_items in this example is not known until calldata is set.

range – Counted Loops

problem.range(start, end) is a context manager yielding a LoopVar. It records a range_start action on entry and a range_end action on exit, and compiles to RANGE/NEXT around whatever the body records. The bounds can be any expression, not just literals or inputs – problem.range(i + 1, n) inside another range(0, n) builds the upper-triangular pattern a pairwise comparison needs, one RANGE nested inside another:

with problem.range(0, n) as i:
    with problem.range(i + 1, n) as j:
        problem.model.quadratic[i, j].add(1)

For n = 3 this adds quadratic[i, j] += 1 for every pair with i < j: (0, 1), (0, 2), (1, 2), each exactly once, matching what running the compiled encoder against n = 3 produces. RANGE is documented at the instruction level in Loops, where it pops count and start, not start and an exclusive end. problem.range(start, end) takes the exclusive end a Python range would; the compiler emits the subtraction that turns it into a count, so a non-zero start shows a SUB before RANGE – compiling problem.range(2, 5) emits PUSH 2 / PUSH 5 / PUSH 2 / SUB / RANGE. A start of 0 hides this, because end and count coincide there, so every RANGE listing so far in this chapter shows no SUB.

iter – Looping Over a Vector’s Elements

problem.iter(vec, start, end) loops over a slice of a vector’s actual elements rather than a count, compiling to ITER/NEXT. It always yields a pair, (idx, val): the element’s absolute position in the vector and its value. Unpack whichever you need and discard the other with _:

with problem.iter(weights, 0, n) as (idx, val):
    problem.model.linear[idx].add(val)

Running this against weights = [7, 8, 9] produces linear = {0: 7, 1: 8, 2: 9}idx supplies the coordinate, val the weight added there. The compiled form pairs LIDX/LVAL inside the loop body with exactly this reading:

PUSH 0
LOAD r0
ITER r1
  LIDX r3
  LVAL r4
  LOAD r3
  LOAD r4
  ADDLINE r2
NEXT

Reach for iter when the loop body needs the element’s value directly and would otherwise call vec.get(idx) on every iteration; reach for range when the body needs an index into something other than the vector being walked, the way knapsack’s objective loop above indexes values_in by position rather than walking it as a slice.

stow – Carrying State Across Iterations

Expressions covers what problem.stow(name, expr) does and why knapsack’s objective loop uses it once per iteration. Inside range and iter, stow has a second job: passing an existing RegLoad back into it, instead of a name, reuses that RegLoad’s register rather than allocating a new one, which is how a loop carries an accumulator from one iteration to the next:

acc = problem.stow("acc", 0)
with problem.range(0, n) as i:
    problem.stow(acc, acc + i)
problem.model.linear[0].add(acc)

The compiled loop body reads and writes the same register, r2, on every iteration, and running it for n = 4 leaves linear[0] = 60 + 1 + 2 + 3:

PUSH 0
STOW r2
PUSH 0
LOAD r0
RANGE
  LVAL r3
  LOAD r2
  LOAD r3
  ADD
  STOW r2
NEXT

branch – Conditional Terms

problem.branch(cond1, body1, cond2, body2, ..., default) compiles to a chain of JUMPI/JUMP/TARGET with first-match semantics. branch tests conditions in order and runs the first true arm’s body; no later arm runs, even one whose condition is also true. The final argument is mandatory and has no condition – pass a callable for a default action, or None to do nothing when no arm matches.

with problem.range(0, n) as i:
    w = problem.stow("w", weights.get(i))
    problem.branch(
        w > 5, lambda: problem.model.linear[i].add(w * 10),
        w > 0, lambda: problem.model.linear[i].add(w),
        None,
    )
LOAD r4
PUSH 5
GT
NOT
JUMPI .1
  LOAD r3
  LOAD r4
  PUSH 10
  MUL
  ADDLINE r2
  JUMP .0
TARGET .1
LOAD r4
PUSH 0
GT
NOT
JUMPI .2
  LOAD r3
  LOAD r4
  ADDLINE r2
  JUMP .0
TARGET .2
TARGET .0

Each condition compiles to itself negated, then a JUMPI past its arm: w > 5 becomes NOT then JUMPI .1, so the arm runs when the condition is true and is skipped straight to TARGET .1 when it is false. An arm that runs ends with JUMP .0, past every remaining arm, which is what makes the semantics first-match rather than last-match. The default arm has no condition and no skip logic – it just falls through to TARGET .0 if reached.

Each body is a zero-argument callable, called once while recording, under the same recording pass a range body runs under, so it can reference i and w from the enclosing scope. Running this for weights = [7, 3, 0] adds 70 at index 0 (7 > 5), 3 at index 1 (3 > 5 is false, 3 > 0 is true), and nothing at index 2 (neither condition holds, and the default is None): linear = {0: 70, 1: 3}, with index 2 absent rather than zero.

Nesting and Ordering

range and iter nest to arbitrary depth, matching the nesting Loops documents at the instruction level: LVAL, LIDX and NEXT always act on the innermost active loop. branch arms can themselves contain range, iter, or further branch calls, since a body callable can record anything a top-level problem definition can.

The encoder splits body actions into an objective block and a constraint block by scanning for constraint calls – see Compiling for that partition. A range or iter body is recorded flatly, so a nested constraint call is still visible to the scan. A branch arm is captured separately: a constraint call made only inside one still constrains the model, but prints under ; === Objective === instead of ; === Constraints ===. Call constraint methods directly inside the enclosing range instead, the way examples/set_cover/runner.py does, if you want the section comment to match.

Outputs and Decoding

A solver hands back a sample: one raw value per variable. Decoding turns that assignment into the answer your problem is actually about – a chosen item set, for knapsack. This page covers a decoder program’s outputs, its read access to the sample, and when decoding outside the VM makes more sense.

Declaring an Output

problem.output(name, type=Types.Vec) declares one decoder output and returns an OutputRef. type must be Types.Vec – every pipeline output is a vector, and problem.output() raises TypeError for anything else. .append(value) grows the output by one element, and the emitted decoder allocates it with VECI before the block that fills it:

selected = problem.output("selected", type=Types.Vec)
with problem.range(0, num_items) as i:
    selected.append(problem.sample.getline(i))

This is examples/knapsack/runner.py’s decoder in full: one output, selected, filled by reading the sample’s per-item assignment directly. problem.sample.getline(i) reads sample[i]: 1 if the solver selected item i, 0 otherwise. selected ends up the same shape as the input item list, a 0/1 flag per item rather than a list of chosen indices. Compiled, this is ; === Decode selected === in the decoder assembly Compiling shows in full: VECI r2, a RANGE loop reading GETLINE r0 and pushing with VECPUSH r2, then PUSH 0 / OUTPUT r2.

OutputRef does not support random-access write or read. Both raise immediately, at problem-definition time – out[i] = value with

TypeError: Random-access write to output 'selected' is not supported; use selected.append(value) instead

and value = out[i] with

TypeError: Reading from output 'selected' is not supported; outputs are write-only via .append(value)

.append(value) is the only way to fill an output, and every output is write-only: the decoder builds each output vector by appending, in the order the decoder program executes, and nothing reads a value back out of one. Knapsack’s decoder above is already the general shape – an .append() per iteration inside a problem.range loop, compiling to VECPUSH against the register the output was allocated with VECI in.

Reading a Sample

problem.sample is available once define_model() has run, and exposes seven read methods built on the Grid Operations and Coefficient Access instructions a Sample register supports:

CallReads
sample.getline(i)Variable i’s assignment (1D or flat)
sample.value(i)Variable i in the domain you declared it over
sample.case(v)Case variable v took, or -1 (categorical)
sample.rowfind(row, value)Column of the first match for value in row (2D)
sample.colfind(col, value)Row of the first match for value in col (2D)
sample.rowsum(row)Sum of every value in row (2D)
sample.colsum(col)Sum of every value in col (2D)

getline and value differ only on a model declared with lo=/hi=, where the stored variable is y = x - lo and value(i) adds lo back to give the x you wrote coefficients over. Everywhere else they are the same call. getline is the only raw reader and value the only shifted one, so nothing shifts unless you ask.

case(v) is rowfind(v, 1) under a name that says what it means on a categorical model: the case variable v took, or -1 where the solver left that row empty. It reads the same on any 2D binary grid, whether or not Domain.CATEGORICAL built it.

Knapsack’s flat model only needs getline. A 2D grid model – one variable per (row, col) pair, the way a one-hot assignment problem is usually shaped – more often needs colfind: “which row has a 1 in this column” reads directly as “which choice was made for this slot.” On a \(2 \times 2\) grid sample with rows [0, 1] and [1, 0], sample.colfind(col=0, value=1) returns 1 (row 1 has the 1 in column 0) and sample.colfind(col=1, value=1) returns 0, each call compiling to the column pushed, then the value to match, then COLFIND r{sample}.

What a Decoder Block May Reference

Everything recorded after the first problem.output() call goes to the decoder and to nothing else, and the decoder is a separate program with a separate register file. It runs on two pieces of calldata: the sample on slot 0, and one scalar on slot 1. Anything a decoder block names that is not reachable from those two is refused at compile() rather than compiled into a read of whatever register happens to hold something.

A block may reference the sample through the five read methods above, its own loop variables, and one scalar. That scalar does not have to be N: examples/graph_coloring/runner.py stows a total variable count before declaring its output and passes that on slot 1. The emitted decoder names which one it resolved, so INPUT r1 ; total_vars tells you what the caller has to supply. Naming a second one raises, because the decoder is handed exactly one and cannot say which of the two you meant:

RuntimeError: xqcp: a decoder block references two scalars, 'n' and 'acc', but the decoder is handed exactly one on calldata slot 1

What a block may not reference is anything the decoder was never given: a vector input or a problem.vec() allocation, a model coefficient (problem.model.linear[i] – the decoder holds the sample, not the model), or a loop variable from a loop that has closed. problem.iter() is refused for the same reason: ITER walks a vector register, and no vector reaches the decoder. Walk the indices with problem.range() and read each one from problem.sample.

One more ordering rule follows from how outputs are emitted. Each output is written to its slot as soon as its own block ends, so appending to an earlier output after a later problem.output() has been declared would land after its target had already shipped. Finish filling one output before declaring the next.

The Decoder Program vs. Decoding in the Host

Once a solver returns a sample, two different things can decode it, and they are not the same operation.

The decoder program is XQVM bytecode: vm.run(programs.decoder) with the sample and N on calldata, exactly like running the encoder or the verifier. It only ever sees the sample through the instructions above, so it runs on any surface that can execute XQVM bytecode – Ways to Use XQuad lists six – with no Python or Rust object model required at the call site.

Decoding in the host means reading the sample object directly in whatever language is driving the pipeline, without running the decoder program at all. In Python, a solved sample is an ordinary XQMX object with a linear dict, so sample.get_linear(i) for each item index reads the same assignments sample.getline(i) does inside the decoder, just from host code instead of from bytecode:

vm.set_calldata([sample, n])
vm.set_output_slots(1)
vm.run(programs.decoder)
result = vm.outputs()[0]

decoder_selection = list(result)                            # via programs.decoder
host_selection = [sample.get_linear(i) for i in range(n)]   # direct read

VM and VMBackend import from xquad.vm, alongside the xquad.cp/xquad.types imports Inputs and Model Shape opens this chapter with. set_output_slots has to run before vm.run, since the slot count defaults to 0. OUTPUT against a slot that was never allocated raises OutputIndex while the decoder runs, on both VMBackend.RUST and VMBackend.PYTHON.

list(result) works whether vm.outputs()[0] comes back as a plain list, on the default VMBackend.RUST, or as a Vec, on VMBackend.PYTHON. Running both against the same seed-42 knapsack sample (weights = [2, 1, 5, 4, 4], values = [5, 4, 18, 3, 19], capacity = 18) produces identical lists: [1, 1, 1, 0, 1]. Host decoding is less code for a one-off script already holding the sample in memory. The decoder program is the version worth keeping once decoding needs to happen the same way regardless of which language or environment is driving the run, or needs to be inspected and verified as an artifact in its own right the way the encoder and verifier already are.

Where This Page Stops

Decoding turns a sample into an answer; it says nothing about whether that answer is any good. valid and energy come from the verifier, not the decoder – see Compiling for what a verifier’s validity check does and does not cover. Choosing a solver and judging solution quality belong to Solving Overview and Running Programs, not here.

Compiling

problem.compile() walks the action list problem.define_model(), your objective terms, your constraints and problem.output() recorded, and returns CompiledPrograms(encoder, verifier, decoder) – three .xqasm strings. Three Programs covers why the split exists and what each program is for; this page covers what compile() actually emits and how to read it when something is wrong.

Three Independent Passes

compile() runs three separate compiler functions over the same action list, each keeping a different subset. Nothing computed for one program carries into another; there is no shared intermediate representation.

Compiling examples/knapsack/runner.py’s build_problem() – the exact code from Constraints, unaltered – produces this encoder:

; === Inputs ===
PUSH 0
INPUT r0
PUSH 1
INPUT r1
PUSH 2
INPUT r2
PUSH 3
INPUT r3

; === Allocations ===
LOAD r0
BQMX r4

; === Objective ===
PUSH 0
LOAD r0
RANGE
  LVAL r5
  LOAD r5
  VECGET r2
  STOW r6
  LOAD r5
  LOAD r6
  NEG
  ADDLINE r4
NEXT
VEC r7
VEC r8
PUSH 0
LOAD r0
RANGE
  LVAL r9
  LOAD r9
  VECPUSH r7
  LOAD r9
  VECGET r1
  VECPUSH r8
NEXT
LOAD r0
LOAD r3
SLACK r7 r8

; === Constraints ===
LOAD r3
PUSH 0x64
EQUALITY r4 r7 r8

; === Output ===
PUSH 0
OUTPUT r4
HALT

PUSH 0x64 is 100, the penalty passed to apply_equality – the compiler renders a handful of common penalty values in hex for readability; every other constant here prints in decimal.

This is the encoder for the exact problem shape examples/knapsack/runner.py builds, independent of the item count: register numbers and instruction count come from how many input(), stow(), vec() and define_model() calls the Python code makes, not from --n at the command line.

What compile_encoder Does

Per spec/xqcp/COMPILER.md, the encoder partitions the action list into inputs, the model allocation, and a body that splits again into objective and constraint blocks: a top-level block is a constraint block if it contains any of onehot_row/onehot_col/exclude/implies/equality/atleast/atleastw/inequality, otherwise it is objective. ; === Objective === and ; === Constraints === are these two blocks, in that order, regardless of the order you wrote them in Python. Knapsack’s objective loop and the SLACK call both come from code written before apply_equality, and neither one is itself a constraint action, so the encoder above places both in the objective section (see Control Flow for the one case, a constraint call inside a branch arm, where this partition surprises). The output section is fixed: PUSH 0 / OUTPUT r{model} / HALT, since an encoder’s only output, always on slot 0, is the model it built.

Register allocation is one incrementing counter shared across the whole program. spec/xqcp/COMPILER.md fixes the order. Inputs go first. A 2D model’s cols register comes ahead of the model register; a 1D model has no cols register, so its model register follows the inputs directly. Loop variables, stowed values, vec() registers and output registers fill in afterward, each claiming a register in the order it is called. Knapsack’s model is 1D: its four inputs claim r0-r3, the model claims r4, and everything after follows call order – r5/r6 for the objective loop’s LoopVar and stow, r7/r8 for the two vec() calls. Inputs and Model Shape shows the 2D case, where the cols register lands before the model register: one input at r0, cols at r1, model at r2. Going past r255 raises RuntimeError at compile time, before any assembly is generated.

What compile_verifier and compile_decoder Do

The verifier replays the encoder. A constraint’s operands – the index and coefficient vectors an apply_equality was handed – are register handles, not data: those vectors are built by VECPUSH instructions that run inside the encoder at VM runtime, often nested in loops. A separate program with its own register file cannot inherit them. So the verifier re-executes the encoder’s inputs, loops, stows, branches and vector construction, drops every model mutation, and emits a check of the sample in place of each constraint, at the same point in the stream. Its register and vector state at each constraint site is then identical to the encoder’s.

That is why the verifier keeps the encoder’s register numbers instead of starting a fixed layout of its own, and why it takes the encoder’s calldata. It claims eight registers above the encoder’s high-water mark: the sample, the valid flag, energy, the model’s declared size, a weighted-sum accumulator, an ITER position, an element, and a counter tracking the index each REDUCE would have allocated. Going past r255 raises RuntimeError at compile time.

Knapsack’s encoder stops at r11, so its verifier’s sample lands on r12 and its valid flag on r13:

; === Inputs ===
PUSH 0
INPUT r0
PUSH 1
INPUT r1
PUSH 2
INPUT r2
PUSH 3
INPUT r3
PUSH 4
INPUT r4
PUSH 5
INPUT r12

; === Model shape ===
LOAD r0
STOW r15

; === Validity checks ===
PUSH 1
STOW r13

; Check every declared variable is in the model's domain
PUSH 0
LOAD r15
RANGE
  LVAL r17
  LOAD r17
  GETLINE r12
  COPY
  PUSH 0
  EQ
  SWAP
  PUSH 1
  EQ
  OR
  LOAD r13
  AND
  STOW r13
NEXT

; === Objective (replayed for state, not for energy) ===
PUSH 0
LOAD r0
RANGE
  LVAL r5
  LOAD r5
  VECGET r2
  STOW r6
NEXT
VEC r7
VEC r8
PUSH 0
LOAD r0
RANGE
  LVAL r9
  LOAD r9
  VECPUSH r7
  LOAD r9
  VECGET r1
  VECPUSH r8
NEXT

; === Constraints ===
PUSH 0
STOW r16
PUSH 0
VECLEN r7
ITER r7
  LIDX r17
  LVAL r18
  LOAD r17
  VECGET r8
  LOAD r18
  GETLINE r12
  MUL
  LOAD r16
  ADD
  STOW r16
NEXT
LOAD r16
LOAD r3
LTE
LOAD r13
AND
STOW r13

; === Energy ===
ENERGY r4 r12
STOW r14

; === Output ===
PUSH 0
OUTPUT r14
PUSH 1
OUTPUT r13
HALT

Three things in that listing are worth reading closely. The model is INPUT into r4, the register the encoder allocated it to, so replayed references resolve without a renumbering pass; BQMX is skipped, but the size expression is replayed and stowed because the domain check needs the bound. The objective loop stowing r6 has no effect on the outcome and runs anyway – a later constraint could read that register, and the replay does not try to work out which stows matter. And the capacity check reads LTE, not EQ: knapsack builds its constraint with slack() followed by apply_equality, and a slack-extended equality is an inequality over the real variables. The SLACK instruction is not replayed, so r7 and r8 hold the four item entries and no slack entries.

The domain check runs to the model’s declared size, r15, replayed from define_model. Slack and REDUCE auxiliary variables live past that size and are not domain-checked. A SPIN model is checked against -1 and +1 instead of 0 and 1; the binary-only constraint kinds – onehot_row, onehot_col, exclude, implies – raise at compile time on a spin model rather than emit a check that does not mean anything there.

The decoder puts the sample on r0 and N on r1, then emits one block per problem.output() call, each starting with VECI to allocate the output vector and ending with PUSH {slot} / OUTPUT r{out}. Inside a decoder block, every scalar reference – an input the encoder read, or a value problem.stow() put in a register – resolves to LOAD r1, because one scalar is all the decoder is handed:

; === Inputs ===
PUSH 0
INPUT r0
PUSH 1
INPUT r1  ; num_items

; === Decode selected ===
VECI r2
PUSH 0
LOAD r1
RANGE
  LVAL r10
  LOAD r10
  GETLINE r0
  VECPUSH r2
NEXT

; === Output ===
PUSH 0
OUTPUT r2
HALT

num_items in with problem.range(0, num_items) as i is an InputRef in the encoder, so in the decoder it becomes LOAD r1, the same register slot 1 was read into two lines above – the decoder has no independent notion of num_items, only of the one scalar it is passed. Which scalar that is, is the program’s choice rather than a fixed N, and the header comment names it. Referencing a second, distinct scalar is rejected at compile(); see Outputs and Decoding for the full list of what a decoder block may and may not name.

The Calldata and Output Contract

Each program’s calldata order and output slots are fixed by what it was compiled from, and a host has to match them exactly:

ProgramCalldata (in order)Outputs
EncoderOne entry per problem.input() call, in call orderSlot 0: the model
VerifierOne entry per problem.input() call, in call order, then the model, then the sampleSlot 0: energy, slot 1: valid
DecoderSample, NOne slot per problem.output() call, in declaration order

examples/knapsack/runner.py’s run() function drives exactly this contract: vm.set_calldata([n, weights, values, capacity]) and vm.set_output_slots(1) before the encoder, matching the four problem.input() calls in build_problem() in order and the encoder’s one output slot; vm.set_calldata([n, weights, values, capacity, model, sample]) and vm.set_output_slots(2) before the verifier; vm.set_calldata([sample, n]) and vm.set_output_slots(1) before the decoder. The output slot count defaults to 0; running a program that executes OUTPUT against a slot that was never allocated raises OutputIndex (see Limits and Errors), on either interpreter.

Problem.verifier_calldata() returns that order as a list of names – ["num_items", "weights", "values", "capacity", "model", "sample"] for knapsack – so a host can zip its own values against it rather than rebuilding the order from the problem definition.

Inspecting the Emitted Assembly

programs.encoder, programs.verifier and programs.decoder are plain Python strings – printing one is the fastest way to see what a problem definition actually compiled to, which is how every listing on this page was produced. From there, the xquad CLI static-inspects a single program without needing a Python host at all: xquad asm and xquad dism round-trip a .xqasm file to bytecode and back to a readable listing, and xquad verify runs the same structural, jump-target, loop-nesting, register type-state and stack-depth checks problem.compile() already runs automatically through xqffi when that package is installed. See CLI for the full command reference; nothing about compiling changes it.

xquad verify and xquad dism work on any of the three programs as they stand, but xquad run does not, because none of the three programs’ inputs are all plain integers – the encoder above needs two vectors on calldata positions 1 and 2, and the CLI’s --calldata flag only accepts a comma-separated list of i64s. Handing it integers where a program expects a vector does not fail to parse; it fails at run time, once the program tries to use the value:

$ xquad run --text knapsack.encoder.xqasm --calldata 2,10,20,5
Error: xqvm::runtime_error

  × register r2 holds int, expected vec<int>

Running any of these three programs against real calldata needs a host that can construct vectors, models and samples – the Python VM/Program surfaces Ways to Use XQuad describes, covered in full in Running Programs. The CLI’s role here is static: assemble, disassemble, and verify a program’s structure before handing it to a host that can supply calldata rich enough to run it.

Running Programs

This page covers driving an already-compiled .xqasm or .xqb program from Python: loading it, supplying calldata, running it, and reading outputs back. Ways to Use XQuad names the surfaces; this page is the full coverage for three of them: xquad.program, and the two VM wrappers underneath and alongside it. For writing a problem with the xqcp DSL, see Modelling. For what xquad verify checks and how to fix a rejected program, see Verification.

Every example on this page ran against the Rust backend, the default for both xquad.program and xquad.vm.VM.

Program and Session

xquad.program.Program loads a program once; Program.session() gives you a Session you call .run() on repeatedly, each time with fresh calldata. Each run returns a RunResult with dict-keyed outputs, the residual stack, and the step count:

from xquad.program import Program

src = """
PUSH 0
INPUT r0
PUSH 1
INPUT r1
LOAD r0
LOAD r1
ADD
STOW r2
PUSH 0
OUTPUT r2
HALT
"""

program = Program.from_source(src)
assert program.instruction_count == 11
assert program.source is not None   # retained for debugging

session = program.session(output_slots=4)
session.set_calldata([40, 2])
result = session.run()

assert dict(result.outputs) == {0: 42, 1: None, 2: None, 3: None}
assert result.stack == []
assert result.steps == 11

# Re-run with different calldata -- sessions carry no hidden state.
session.set_calldata([100, 200])
assert dict(session.run().outputs) == {0: 300, 1: None, 2: None, 3: None}

RunResult.outputs is a dict keyed by slot index; a slot the program never wrote reads as None rather than raising. Program is immutable and reusable; Session carries the mutable calldata and output-slot count. Deriving a second Session from the same Program gives you two runners that share no state.

Step limits

Session.set_step_limit(n) caps how many instructions a run may execute before run() raises. The limit is exact: 0 permits no instructions at all. A fresh Session starts at xquad.vm.DEFAULT_STEP_LIMIT, which is 10,000,000. None is the only unbounded spelling, and a caller has to write it – unbounded execution is asked for, never inherited.

session = program.session(output_slots=1)
session.set_calldata([1, 2])
assert "step_limit=10000000" in repr(session)

session.set_step_limit(3)
try:
    session.run()
except RuntimeError as e:
    assert "StepLimitExceeded" in str(e)

# 0 is not a sentinel for "unlimited" -- it executes nothing, so this
# 11-step program fails at its first instruction.
session.set_step_limit(0)
try:
    session.run()
except RuntimeError as e:
    assert "StepLimitExceeded" in str(e)

# None removes the bound. A program that never halts will not return,
# and it does not take SIGALRM: the GIL is held inside the Rust run.
session.set_step_limit(None)
assert "step_limit=unlimited" in repr(session)
assert dict(session.run().outputs) == {0: 3}

A budget that exactly covers the program succeeds. This one needs 11 steps, so set_step_limit(11) runs it and set_step_limit(10) does not.

Loading bytecode directly

Program.load(bytes) parses raw wire-format bytes – a .xqb blob produced by xquad asm or by program.bytecode(). No source is retained:

blob = program.bytecode()
reloaded = Program.load(blob)
assert reloaded.source is None
assert reloaded.instruction_count == program.instruction_count

Program.load never validates its input; a malformed blob decodes successfully as a Program object and only fails once a Session tries to run it. A one-byte blob is too short even for the wire-format header:

p = Program.load(b"\x43")
s = p.session()
try:
    s.run()
except RuntimeError as e:
    print(e)
# decode error: TruncatedHeader

Program.from_source validates at load time instead: an unassemblable string raises ValueError immediately, before a Session exists.

Calldata types

A calldata list may mix any of int, list[int], xqffi.vm.XqmxModel, xqffi.vm.XqmxSample, and None (unset). Session.set_calldata checks every element’s type as soon as you call it, not at run time:

from xqffi.vm import XqmxModel, XqmxSample

model = XqmxModel("binary", size=4)
model.set_linear(0, -1)
model.set_quad(0, 1, 2)

sample = XqmxSample("spin", values=[-1, 1, -1, 1])

session.set_calldata([model, sample, [1, 2, 3], 42])
# session.run() now sees four typed input slots.

try:
    session.set_calldata([object()])
except TypeError as e:
    print(e)
# unsupported calldata element type: object; expected int, list[int],
# XqmxModel, XqmxSample, or None

Inspecting a model or sample

XqmxModel and XqmxSample are pyo3 objects; there is no dict-conversion helper in this package today. Read a model’s coefficients through its own accessors:

model = XqmxModel("binary", size=4)
model.set_linear(0, -1)
model.set_linear(2, 3)
model.set_quad(0, 1, 2)

model.domain          # "binary"
model.size            # 4
list(model.linear_items())     # [(0, -1), (2, 3)]
list(model.quadratic_items())  # [((0, 1), 2)]
model.get_linear(0)   # -1
model.get_quad(0, 1)  # 2
repr(model)            # 'XqmxModel(domain=binary, size=4)'

__repr__ is intentionally minimal. Convert to xquad.types.XQMX (the canonical, backend-independent type covered next) if you want a Python object you can inspect with normal attribute access, or serialise by iterating linear_items() / quadratic_items() yourself.

The other two VM surfaces

Two lower-level wrappers sit underneath and alongside xquad.program, both worth knowing about directly.

xqffi.vm.Vm is the thinnest possible wrapper over the Rust interpreter: construct it, call set_calldata / set_output_slots, call .run(bytecode), read .outputs() as a positional list. It always targets the Rust backend. .reset() clears the stack, registers, loop stack, and step counter, but calldata and output slots are untouched by it – both persist across runs until you call set_calldata() or set_output_slots() again. It is what Session.run() builds internally:

from xqffi.vm import Vm

vm_src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
bytecode = Program.from_source(vm_src).bytecode()

vm = Vm()
vm.set_output_slots(1)
vm.run(bytecode)
assert vm.outputs() == [12]

Ways to Use XQuad covers when to reach for it directly instead of Session.

xquad.vm.VM is a separate wrapper that every example runner under examples/ actually uses (examples/maxcut/runner.py, examples/knapsack/runner.py). It selects between the Rust interpreter and the pure-Python reference VM via VMBackend, converts FFI objects to and from the canonical xquad.types.XQMX at the boundary, and runs .xqasm source text directly rather than pre-assembled bytecode:

from xquad.vm import VM, VMBackend

src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
v = VM(backend=VMBackend.RUST)  # RUST is also the default with no argument
v.set_output_slots(1)
v.run(src)
assert v.outputs() == [12]
assert v.stack() == []

VM.outputs() returns xquad.types.XQMX instances for model and sample outputs, where xqffi.vm.Vm.outputs() and Session.run().outputs return the raw pyo3 XqmxModel / XqmxSample objects. Pick VM when you need Python-backend parity checking or when your calldata and outputs are already in xquad.types terms; pick Session for the dict-keyed, slot-sparse ergonomics shown above; pick xqffi.vm.Vm only if you are building your own layer on top of the raw Rust FFI.

A complete run across three programs

A compiled problem is three independent programs – see Three Programs for why. Driving all three from xquad.program looks like this, using the exact build_problem function examples/knapsack/runner.py defines (see Compiling for what compile() emits):

from examples.knapsack.runner import build_problem
from xquad.program import Program
from xqffi.vm import XqmxSample

n = 4
weights = [2, 3, 4, 5]
values = [3, 4, 5, 8]
capacity = 8

problem = build_problem(n, weights, values, capacity)
programs = problem.compile()

# Encoder: inputs in, model out.
encoder = Program.from_source(programs.encoder)
enc_session = encoder.session(output_slots=1)
enc_session.set_calldata([n, weights, values, capacity])
model = enc_session.run().outputs[0]

# A hand-picked sample -- items 1, 2, 3 selected, no solver involved.
sample = XqmxSample("binary", values=[0, 1, 1, 1, 0, 0, 0, 0])

# Verifier: the encoder's own inputs, then model and sample; (energy, valid) out.
verifier = Program.from_source(programs.verifier)
ver_session = verifier.session(output_slots=2)
ver_session.set_calldata([n, weights, values, capacity, model, sample])
ver_result = ver_session.run()
energy, valid = ver_result.outputs[0], ver_result.outputs[1]

# Decoder: sample + N in, decoded selection out.
decoder = Program.from_source(programs.decoder)
dec_session = decoder.session(output_slots=1)
dec_session.set_calldata([sample, n])
selected = dec_session.run().outputs[0]

assert (energy, valid, selected) == (-4817, 0, [0, 1, 1, 1])

Items 1, 2 and 3 weigh 3 + 4 + 5 = 12 against a capacity of 8, so the sample violates the capacity constraint and valid comes back 0. energy is still computed: ENERGY recomputes the objective whatever the sample’s feasibility, so the two outputs answer different questions. Swap in [1, 1, 0, 0, 0, 0, 0, 0] – items 0 and 1, weighing 5 – and the same run gives (-5507, 1). Verification says what valid covers.

The verifier takes the encoder’s calldata because it replays the encoder to rebuild the constraint data, then appends the model and the sample. Each program’s own .set_calldata order and .set_output_slots count are fixed by what it was compiled from – Compiling has the full table. A real pipeline replaces the hand-picked sample above with xqsa.build_solver(...).solve(model).sample; see Solving Overview.

What this page does not cover

  • Tracing. Step-by-step execution inspection is not part of Session today.
  • Keyword calldata (session.set_calldata(n=4, ...)). Session takes a positional list only; there is no input-slot labelling to key against.
  • A _repr_html_ for notebooks. __repr__ is the only representation Program, Session, RunResult, XqmxModel and XqmxSample provide.
  • A to_numpy() helper. Convert via linear_items() / quadratic_items() and numpy.asarray(...) on your own side; this package does not carry a numpy dependency.

Verification

xquad verify rejects a program before it runs, printing the first structural or semantic problem the bytecode verifier finds. This page takes you from that message to a working program. Verifier is the reference half, covering what each phase checks and what every error means. This page does not repeat that; it shows each error actually firing and what to change.

xquad verify --text file.xqasm assembles the source in memory and verifies the result; xquad verify file.xqb verifies pre-assembled bytecode. Both forms and their exit codes are covered in CLI: verify; this page assumes you already know how to invoke it.

Reading an error

Every example below is the smallest program that reproduces its error, followed by the exact xquad verify output it produces.

Structural errors

BadOpcode – a byte in the instruction stream does not map to any of the 93 known instructions. This cannot come from a .xqasm file the assembler accepted; it means a .xqb file was hand-edited or corrupted:

Error:   × unknown opcode 0x0d at byte 0x0000

Fix: reassemble from source rather than editing bytecode by hand, and check the .xqb file was not truncated or modified in transit.

TruncatedInstruction – the byte stream ends partway through an instruction’s operand bytes. A PUSH1 opcode byte with no value byte after it triggers it:

Error:   × truncated instruction at byte 0x0000

Fix: same as BadOpcode – the .xqb file is not a complete, valid encoding. Reassemble it.

Jump-target and loop-nesting errors

UndefinedJumpTarget – a jump instruction’s label id is at or past the program’s target count. Text assembly cannot produce this: the assembler resolves every .N label reference against the labels actually defined in the source and refuses to emit a jump to one that does not exist – xqasm::undefined_label, at assemble time, before the verifier ever runs. UndefinedJumpTarget is a defense for bytecode assembled some other way, or corrupted after assembly: a jump instruction’s label-id byte, changed to a value the program’s target count does not cover, produces:

Error:   × jump at byte 0x0003 references undefined target label 5 (program has 1
  │ targets)

Fix: this is not something valid .xqasm can trigger, so seeing it means the .xqb file in hand is not what the assembler produced. Reassemble from source.

NoActiveLoopNEXT, LVAL, or LIDX appears with no RANGE or ITER open at that point:

NEXT
HALT
Error:   × loop instruction at byte 0x0000 executed outside any active loop

Fix: every NEXT, LVAL, and LIDX needs an enclosing RANGE/ITER on every path that reaches it – check you have not placed one outside the loop body, or past a jump that skips the loop opener.

UnmatchedLoop – a RANGE or ITER opens with no matching NEXT before the program ends:

PUSH 0
PUSH 3
RANGE
HALT
Error:   × unmatched loop: RANGE/ITER at byte 0x0004 has no corresponding NEXT (1
  │ loop(s) still open at end of program)

Fix: add the missing NEXT. If this program came from xqcp, it usually means a with problem.range(...) block (see Control Flow) whose body raised or returned before the DSL closed it correctly – check the generated .xqasm for the loop in question.

Register errors

ReadUnsetRegister – a register is read before anything writes it, or before every incoming control-flow path writes it (must-init analysis, phase 3, catches the second case even when phase 2’s type check alone would not):

LOAD r0
HALT
Error:   × register r0 read at byte 0x0000 before being written

Fix: write the register before reading it. If the read follows a branch, confirm both arms write it – a register set on only one side of a conditional is ReadUnsetRegister at the first read after the join, even though nothing about the type looked wrong on either arm individually. DROP also resets a register to unset; a LOAD right after DROP on the same register fails the same way.

RegisterTypeMismatch – a register holds a RegVal variant other than the one the instruction needs. Here BQMX writes a Model, and LOAD only handles Int:

PUSH 4
BQMX r0
LOAD r0
HALT
Error:   × register r0 at byte 0x0004: expected int, got model

Fix: LOAD is for scalar integers. To move a Model out of a register for inspection or as another instruction’s operand, pass the register directly to an instruction that accepts Model (GETLINE, ENERGY, and others); do not route it through LOAD.

Stack-depth errors

StackUnderflow – a reachable instruction would pop more items than the analysis can prove are on the stack:

ADD
HALT
Error:   × stack underflow at byte 0x0000

Fix: push the operands an instruction needs before it runs. This same check also catches loop-related shortfalls; see the “does not guarantee” section below for its blind spot.

StackOverflowRisk – the analysis converges on a depth past the 8,192-item limit for some reachable block. A straight-line run of 8,200 PUSH instructions with no loop involved:

Error:   × potential stack overflow at byte 0x0000 (depth 8200)

Fix: this fires on a single basic block’s static depth, so the usual cause is building up a large flat structure with individual PUSH/VECPUSH calls rather than a loop. Reducing the item count, or restructuring the build as a loop the analysis can bound per iteration, both work; see the next section for why a loop is not automatically safe either.

LoopStackImbalance – a loop body’s net stack effect is non-zero: the depth the analysis computes at NEXT differs from the depth at the matching RANGE/ITER. An unmatched PUSH inside the loop body:

PUSH 0
PUSH 3
RANGE
PUSH 99
NEXT
HALT
Error:   × loop starting at byte 0x0004 has non-zero stack effect (entry depth 0,
  │ exit depth 1)

Fix: every value a loop body pushes needs a matching pop (directly, or via STOW into a register) before NEXT, so each iteration leaves the stack exactly as it found it. SCLR inside a loop body is a special case: it resets the tracked depth to zero unconditionally, so this error fires when the loop’s entry depth was already non-zero, not when the body’s depth before the reset was non-zero. If the entry depth was zero, the same reset can instead mask a real imbalance earlier in the body, and the mismatch surfaces later, at the next downstream join point, rather than here.

StackDepthMismatch – two control-flow paths reach the same join point with different stack depths, so the depth at that point is not well-defined. A conditional that pushes on only one arm:

PUSH 1
JUMPI .0
PUSH 2
.0: HALT
Error:   × stack depth mismatch at join point byte 0x0006: one path has depth 0,
  │ another has depth 1

Fix: make every arm of a branch leave the same stack depth before it rejoins. If only one arm pushes a value, move that push above the branch so both arms inherit it.

What passing verification does not guarantee

A pass means every phase’s static checks succeeded. It does not mean the program runs to completion. The stack-depth phase reasons about each basic block’s net effect, so an instruction that pops more operands than it pushes has its pop requirement absorbed whenever the running depth stays non-negative – an operand-ordering error is invisible to it. PUSH 1 / ADD / HALT passes because the scan sees a net effect of +1 - 1 = 0 for the two instructions together, not that ADD needs two operands and only one was ever pushed:

$ xquad verify --text add_underflow.xqasm
ok: add_underflow.xqasm (3 instructions)
$ xquad run --text add_underflow.xqasm
Error: xqvm::runtime_error

  × stack underflow at byte 0x0002
   ╭─[add_underflow.xqasm:2:1]
 1 │   0x0000:  PUSH1   1
 2 │   0x0002:  ADD     
   · ─────────┬─────────
   ·          ╰── execution failed here
 3 │   0x0003:  HALT    
   ╰────

This is a documented, current limitation of the per-basic-block analysis, not a bug specific to this program. Verifier: what passing verification guarantees states the precise scope. Treat a pass as “structurally sound,” not as “will run to completion.”

A whole class of faults is outside the verifier’s reach for a different reason: they depend on values, and the verifier tracks types and depths rather than values. An allocator size, a grid extent, a loop bound, a calldata index and every arithmetic operand are ordinary popped stack values, so InvalidAllocation, InvalidGridDimensions, InvalidIntegerK, ArithmeticOverflow, IndexOutOfBounds, SampleOutOfDomain, LoopStackOverflow, StepLimitExceeded and MemoryLimitExceeded are all runtime faults with no static counterpart. SampleOutOfDomain is the clearest case of why: the value written into a sample can arrive from calldata, so no amount of static analysis can know it. A verified program can still raise any of them, and for an embedder that is the point: the budgets and the range checks are what bound a program the verifier has already passed. See Limits and Errors.

The generated verifier program is not the bytecode verifier

xquad verify and the verifier program are two different things that share a name by coincidence of vocabulary.

xquad verify runs the bytecode verifier described above and in Verifier: a static analysis over any .xqasm or .xqb file, checking that the program is well-formed before anything executes. It has no idea what problem, if any, the program encodes.

A compiled problem’s verifier program is one of the three .xqasm outputs problem.compile() returns – see Three Programs and Compiling. It is an ordinary program like any other: xquad verify can check it is well-formed, the same as it checks the encoder or the decoder. What the verifier program itself does at runtime – taking a model and a candidate sample as calldata and producing (energy, valid) – is domain logic the DSL emitted, unrelated to the bytecode verifier’s job. Running the verifier program does not verify a program in the bytecode-verifier sense, and running xquad verify against the verifier program’s .xqasm text does not check whether a sample is a good answer.

What the generated verifier’s valid flag covers

valid is the conjunction of one check per constraint the problem declared, plus a domain check over every declared variable. A sample that violates an onehot_row, onehot_col, equality, inequality, atleast, atleastw, exclude, implies, or the Rosenberg auxiliary a reduce() allocated, comes back valid = 0. Compiling shows how each check is emitted.

The checks are over the variables the problem declared, not over the encoding. slack() extends an equality’s index vector with the encoder’s own variables; the verifier checks that constraint as sum <= bound over the real variables and leaves the slack bits unconstrained. Checking the expanded form instead would report valid = 0 for a feasible sample whose slack bits a solver happened to leave inconsistent, which on a settlement path means refusing to pay for a correct answer.

Two things valid = 1 does not claim. The domain check runs to the model’s declared size, so slack and REDUCE auxiliaries allocated past that size are not domain-checked. And valid says nothing about optimality: ENERGY recomputes the objective independently of where the sample came from, and a feasible sample can still be a poor one.

Solving Overview

Backends names the five solvers and what distinguishes them. This chapter covers each one in enough depth to run it: installation, parameters, and what its result actually contains.

xqsa is the package that does this. It defines one abstract interface, Solver, and five classes built on it – SolverDWaveCPU, SolverDWaveQPU, SolverCudaGPU, SolverMetalGPU, and SolverQuip. A model built from xqcp or by hand in XQVM bytecode does not know or care which one samples it.

The Solver Interface

Every backend defines the same single method:

class Solver(ABC):
    @abstractmethod
    def solve(self, model: XQMX, **kwargs: Any) -> SolverResult: ...

model is an XQMX in MODEL mode, binary or spin domain. **kwargs override that solver’s constructor defaults for this call only – solver = SolverDWaveCPU(num_reads=100) then solver.solve(model, num_reads=500) runs 500 reads without building a new solver. solve() raises on failure (hardware unreachable, no embedding found, connection lost); it never returns a partial or empty result.

The return value is a frozen dataclass:

@dataclass(frozen=True)
class SolverResult:
    sample: XQMX
    energy: int
    timing: float
    metadata: dict[str, Any] = field(default_factory=dict)

sample is the solution, an XQMX in SAMPLE mode. energy is the authoritative Hamiltonian energy. timing is wall-clock seconds spent solving. metadata defaults to an empty dict; its actual shape is per-backend, covered below.

energy is never the backend’s own reported value taken on faith. Every solve() recomputes it with the same integer formula the XQVM ENERGY opcode uses, over the model and the returned sample. See Energy and Precision for what that buys you and what it costs on hardware with limited precision.

metadata’s shape is not identical across all five backends. The three classical simulated-annealing backends – dwave-cpu, cuda-gpu, and metal-gpu – carry seed and reads at the top level plus a params dict of solver-specific detail (sweep counts, the raw pre-recompute energy). dwave-qpu carries a params dict the same way, but omits seed (there is no seed on physical hardware) and adds solver and qpu_timing at the top level, alongside reads; see D-Wave QPU. quip returns a different set entirely – no seed, reads, or params at all; see Quip Network. Check the backend you are calling before indexing into result.metadata.

Picking a Backend by Name

build_solver constructs any of the five from a short string, so a caller – an example runner, a CLI flag, a script – can stay backend-agnostic:

from xqsa import SOLVERS, DEFAULT_SOLVER, build_solver

print(sorted(SOLVERS))
# ['cuda-gpu', 'dwave-cpu', 'dwave-qpu', 'metal-gpu', 'quip']
print(DEFAULT_SOLVER)
# dwave-cpu

solver = build_solver("dwave-cpu", seed=42)
result = solver.solve(model)

seed reaches the three classical simulated-annealing backends (dwave-cpu, cuda-gpu, metal-gpu) and is ignored for dwave-qpu (physical hardware has no seed) and quip (configured from the environment; the miner’s own randomness is out of the caller’s control).

build_solver otherwise uses each backend’s own constructor defaults, with one exception: for cuda-gpu and metal-gpu it raises num_reads to 200 and num_sweeps to 2000, rather than the 100 and 1000 the bare constructor defaults to. Local Solvers has the constructor defaults for every backend.

Swapping Backends Without Changing the Model

This is the claim the whole toolchain rests on, so here it is checked rather than asserted. examples/maxcut/runner.py builds one Max-Cut model with xqcp, runs the encoder on the Rust XQVM, and samples the result with whichever solver --solver names, built through build_solver. Run it against two different backends – dwave-cpu (CPU simulated annealing, 100 reads and 1000 sweeps) and metal-gpu (an Apple Silicon GPU kernel, a different process on different hardware, and – per build_solver’s override above – 200 reads and 2000 sweeps) – and nothing about the model or the encoder changes:

uv run python examples/maxcut/runner.py --n 6 --seed 42 --interpreter rust --solver dwave-cpu
uv run python examples/maxcut/runner.py --n 6 --seed 42 --interpreter rust --solver metal-gpu

Both print the same result: "energy": -571, "cut_weight": 571, "valid": 1, the identical partition. The encoder produced one QUBO; two unrelated solvers minimised it and agreed. That agreement is not guaranteed in general – a harder model can leave different backends in different local optima – but the model they were handed, and the verifier that checked what came back, never changed.

The Domain Every Backend Rejects

XQVM has three domains: binary, spin, and integer. Every current xqsa solver’s _validate_model() accepts binary and spin and raises ValueError on integer:

from xqsa import SolverDWaveCPU
from xqvm_py.xqmx import XQMX

model = XQMX.integer_model(size=3, k=4)
SolverDWaveCPU().solve(model)
# ValueError: Unsupported domain for solving: INTEGER

An integer XqmxModel is a real thing you can build in XQVM bytecode today – see Three Domains – but nothing in this chapter can solve one. spec/xqsa/DOMAINS.md calls this “reserved”: a future solver may relax the check, but none of the current five does. Re-encoding the problem in binary or spin is the only route around it today.

Choosing Among the Rest

You wantRead
A baseline that runs anywhere, no hardware or credentials requiredLocal Solvers, dwave-cpu
A local GPU for a bigger or faster runLocal Solvers, cuda-gpu / metal-gpu
Real quantum annealing hardwareD-Wave QPU
A decentralised, miner-solved compute marketQuip Network
What the numbers in SolverResult actually meanEnergy and Precision

That baseline requirement – runs anywhere, needs nothing – is exactly DEFAULT_SOLVER: every other backend is worth comparing against dwave-cpu before you trust its answer.

Local Solvers

Three of the five backends run entirely on the machine calling them, using only local hardware and no account. dwave-cpu runs on the CPU; cuda-gpu and metal-gpu run custom simulated-annealing kernels on an NVIDIA or Apple Silicon GPU. The metal-gpu examples below ran for real, on an Apple Silicon Mac; no NVIDIA GPU was available while writing this chapter, so the cuda-gpu material comes from reading xqsa/cuda_gpu.py rather than from execution.

dwave-cpu: The Baseline

SolverDWaveCPU wraps dwave-samplersSimulatedAnnealingSampler. It needs only a CPU and no credentials, and that is exactly what makes it xqsa.DEFAULT_SOLVER: every other backend in this chapter is worth comparing against it before you trust a faster or more exotic answer. It ships in the base package – pip install xqsa is enough.

from xqsa import SolverDWaveCPU
from xqvm_py.xqmx import XQMX

model = XQMX.binary_model(size=4)
model.set_linear(0, -1)
model.set_quadratic(0, 1, 2)

solver = SolverDWaveCPU(seed=42)
result = solver.solve(model)
print(result.energy, result.metadata)

Constructor parameters, all overridable per call via solve(**kwargs):

ParameterDefaultMeaning
num_reads100Independent annealing runs; the best is kept
num_sweeps1000Sweeps per run
num_sweeps_per_beta1Sweeps held at each temperature level before cooling; num_sweeps must divide evenly
beta_rangeNoneInverse-temperature (start, end); None lets dwave-samplers pick
seedNoneRNG seed; None means non-deterministic

GPU Backends: cuda-gpu and metal-gpu

SolverCudaGPU and SolverMetalGPU bypass the D-Wave SDK entirely. Both convert the model to a dense array and run parallel-replica simulated annealing directly on the GPU, in custom CUDA or Metal kernels. Picking between the CPU backend and either GPU backend is a performance question, not a correctness one – see Backends; all three run the identical annealing algorithm family, just on different processors.

Install the extra that matches your hardware:

pip install xqsa[cuda]     # NVIDIA GPU, CUDA 12.x driver
pip install xqsa[metal]    # Apple Silicon Mac with a Metal device
from xqsa import SolverMetalGPU
from xqvm_py.xqmx import XQMX

model = XQMX.binary_model(size=4)
model.set_linear(0, -1)
model.set_quadratic(0, 1, 2)

solver = SolverMetalGPU(strategy="sa", num_reads=200, num_sweeps=2000, seed=42)
result = solver.solve(model)
print(result.energy)
# -1

Wall-clock result.timing is not shown here: it varies by roughly 2x between runs on identical hardware, so a single pasted number would read as a performance claim it cannot support. metal-gpu also supports strategy="gibbs" (block Gibbs sampling over a greedy graph colouring of the coupling graph). Both strategies produce correct output on real Metal hardware: xqsa/tests/test_gpu_validation.py checks each against models with a known exact ground state, plus a spin-glass model matched within 2% of the CPU reference energy, all six of which pass.

cuda-gpu supports only strategy="sa" – CUDA has no Gibbs kernel. See xqsa/cuda_gpu.py for the implementation the rest of this section describes.

Parametercuda-gpu defaultmetal-gpu defaultMeaning
strategy"sa" (only option)"sa" or "gibbs"Kernel to dispatch
num_reads100100Parallel replicas, one per GPU threadgroup/block
num_sweeps10001000Sweeps per replica
num_sweeps_per_beta11Sweeps held per temperature level before cooling
beta_schedule_type"linear""geometric"Shape of the cooling schedule – note the two backends default to different shapes
beta_rangeNoneNoneInverse-temperature (start, end); None auto-computes it from the model’s coefficients
seedNoneNoneBase seed for the on-device RNG

These are the constructor’s own defaults. xqsa.build_solver, the backend-agnostic constructor covered in Solving Overview, raises num_reads to 200 and num_sweeps to 2000 for both GPU backends instead of using these defaults.

metal-gpu computes coefficients in float32; cuda-gpu computes them in float64, end to end in its kernels. result.energy is always recomputed afterward in exact integer arithmetic on both, so either GPU run and a CPU run report the same authoritative energy regardless of which precision the search itself used. See Energy and Precision for what the float32 downcast costs metal-gpu and when it matters.

Driver Prerequisites

ExtraHardwarePrerequisiteVerify
[cuda]NVIDIA GPUCUDA 12.x drivernvidia-smi
[metal]Apple SiliconA Metal devicebuilt in on every Apple Silicon Mac

After installing, confirm the solver class constructs before building a model around it:

python -c "from xqsa import SolverCudaGPU; SolverCudaGPU(); print('ok')"
python -c "from xqsa import SolverMetalGPU; SolverMetalGPU(); print('ok')"

Both raise ImportError with a pip install xqsa[...] hint if the extra is missing, and RuntimeError if the extra is installed but no matching GPU is detected – import xqsa itself never fails on a missing extra, only constructing the solver that needs it does.

D-Wave QPU

SolverDWaveQPU submits the model to a physical D-Wave Advantage quantum annealer over the D-Wave Leap cloud API. This page is derived from xqsa/dwave_qpu.py and spec/xqsa/*; no D-Wave Leap account was available while writing it, so nothing on this page was run.

Credentials

Requires the [dwave] extra:

pip install xqsa[dwave]

SolverDWaveQPU() resolves an API token from the token= constructor argument first, then the DWAVE_API_TOKEN environment variable; with neither set, construction raises ValueError. An optional endpoint= or DWAVE_API_ENDPOINT overrides the default Leap API URL.

import os
os.environ["DWAVE_API_TOKEN"] = "your-leap-token"

from xqsa import SolverDWaveQPU
from xqvm_py.xqmx import XQMX

model = XQMX.binary_model(4)
model.set_linear(0, -1)
model.set_quadratic(0, 1, 2)

solver = SolverDWaveQPU()              # auto-selects a Pegasus-topology Advantage system
result = solver.solve(model)
print(result.metadata["solver"])       # e.g. "Advantage_system5.4"
print(result.metadata["qpu_timing"])   # QPU timing breakdown from Leap

Pass solver="Advantage_system5.4" to target a specific system instead of the default Pegasus-topology filter.

What Embedding Means

A D-Wave Advantage chip is not fully connected: each physical qubit couples only to a fixed, small set of neighbours defined by its Pegasus topology. Your model’s coupling graph is almost never a subgraph of that hardware graph directly. SolverDWaveQPU wraps the sampler in dwave.system.EmbeddingComposite, which finds a minor embedding: each logical variable maps to a chain of one or more physical qubits, wired together so the chain acts as a single variable by strongly coupling its members (chain_strength).

Problem size on real hardware is not simply “how many variables.” A densely coupled model needs longer chains to embed, chains compete for the chip’s limited qubits, and a model that does not embed at all raises rather than silently degrading. EmbeddingComposite handles the search automatically; there is no separate embedding step to call in xqsa. Contrast this with Quip Network, where SolverQuip requires an exact subgraph match onto a fixed topology and refuses to do this chain-based embedding at all.

Parameters

ParameterConstructor defaultMeaning
tokenNone (env fallback)Leap API token
endpointNone (env fallback)Leap API endpoint override
solverNone (Pegasus filter)Specific Advantage system name
num_reads100Annealing runs read back from the chip
annealing_time20Microseconds per anneal

Both num_reads and annealing_time are overridable per call: solver.solve(model, annealing_time=100, chain_strength=2.0). chain_strength and other EmbeddingComposite options pass straight through **kwargs to the underlying sampler call.

How a QPU Result Differs

The result shape is the same SolverResult every backend returns, but two things are specific to this backend. result.energy is still recomputed by xqsa in exact integer arithmetic from the returned sample – the chip’s own reported energy is never trusted directly – but the sample search itself ran on physical hardware subject to analog noise and the embedding above, not an exact digital simulation. And result.metadata carries QPU-specific detail that the local backends do not report at all: metadata["solver"] names the physical Advantage system that ran the job, and metadata["qpu_timing"] is the timing breakdown Leap returns – a dict when present, with keys that come from Leap and are not fixed by xqsa, or None when the sampler response carries no timing info at all. solver, qpu_timing, and reads sit at the top level of metadata, alongside a params dict structured the same way it is for the three simulated-annealing backends (params["annealing_time"], params["raw_energy"]) – only solver and qpu_timing sit outside params, and this call does not set metadata["seed"] at all (there is no seed on physical hardware). See Solving Overview for the full, per-backend split. If you write code against result.metadata for more than one backend, do not assume its shape is identical across all five; check the backend you are calling.

Quip Network

SolverQuip is the one backend in this chapter that does not solve anything itself. It proposes the model as a job to the Quip Network’s QuantumComputeMempool pallet, waits for a miner – hardware it does not control, running an algorithm it does not choose – to solve it, and decodes whatever comes back as an XQMX sample.

This page is derived from xqsa/quip.py, spec/xqsa/SOLVERS.md, and an internal contributor testing guide. The mechanism it describes has been exercised end to end against a live Quip deployment, but the coordinates for reaching one are not public – see Gaps for what that leaves unanswered for a reader.

Installation and Configuration

Requires the [quip] extra:

pip install xqsa[quip]

The umbrella xquad package forwards [cuda], [dwave], and [metal] to the matching xqsa extra, but has no [quip] extra of its own – install it against xqsa directly. The extra brings in substrate-interface (the chain RPC client) and quip-signer, a native extension providing the chain’s hybrid signature scheme (sr25519 plus FN-DSA-512), which substrate-interface alone cannot produce. quip-signer resolves a prebuilt wheel on Linux and builds from source with a local Rust toolchain on macOS and Windows. Both guards are lazy: import xqsa and every other solver in this chapter work without the extra installed; only constructing SolverQuip needs it.

Configuration resolves from constructor arguments first, then environment variables:

ArgumentEnvironment variableMeaning
urlQUIP_RPC_URLWebsocket RPC endpoint
seedQUIP_SIGNER_SEED32-byte hex master seed
keystoreQUIP_KEYSTOREKeystore file path (loaded, or created on first use)
rewardQUIP_REWARDReward in planck; falls back to the chain’s MinReward

Provide exactly one of seed or keystore. spec_id and topology are constructor-only overrides – both default to chain state (DefaultIsingSpecId and DefaultTopology) and have no environment variable. mode, resolution, and delivery are pass-through job parameters defaulting to Open, SingleBest, and OnChainOnly; the pallet’s data-carrying variants (Callback delivery, Bid mode) exist on-chain but SolverQuip does not exercise them.

Job Lifecycle

solve() calls propose_job, which reserves the full reward and emits a JobProposed event. Miners discover work only from that live event stream – there is no storage backfill, so a job proposed before a miner subscribed stays invisible to it. A miner that accepts solves it on its own hardware with its own algorithm and submits a solution.

No on-chain hook closes an order; its status changes only when an extrinsic touches it. SolverQuip computes finality itself from block height instead of waiting for an OrderClosed event:

effective_expiry = min(created_at + deadline_blocks, first_solution_at + block_wait)

solve() polls until the chain head reaches that height, then reads the result. query(order_id, model, *, mapping=None, topology=None) returns None before finality, and a full SolverResult after – not a bare sample: order_id, solver, and the rest of the fields in Metadata come with it. Pass mapping=/topology= only when they were non-default at proposal time; they let the order be read back from a different process, for example after catching a QuipTimeoutError, using the same deterministic placement. status(order_id) is a separate, single-read snapshot that does not wait and never decodes a result. Both solve() and query() share the same finalization path: if an order finalizes with zero solutions, the reward is best-effort reclaimed via reclaim_order (a failed reclaim is logged, not raised) and QuipJobFailedError is raised.

The Topology Constraint

Real annealing hardware – and the miner fleet behind this network – has a fixed set of physical couplings. SolverQuip skips the chains and minor embedding that D-Wave QPU’s EmbeddingComposite builds, and instead requires subgraph placement: an injective variable-to-node mapping where every one of the model’s couplings maps onto a real hardware edge. A model whose coupling graph is not a subgraph of the target topology raises PlacementError, carrying the couplings that could not be placed. Placement is deterministic, computed from the model alone, so nothing about it needs to be persisted or looked up later.

The target topology is resolved once, at construction, from three sources in order: the topology= argument, the QUIP_TOPOLOGY environment variable, then the chain’s QuantumPow.DefaultTopology. Because the same topology shape hashes differently per deployment – each network’s allowed-value specifications fold into the hash – a hash from one Quip deployment is not portable to another. That is why nothing is pinned in the codebase: every source is deployment-local, and a topology that resolves from none of them raises rather than selecting a hash no chain would accept. QUIP_TOPOLOGY is the operator’s override for whichever chain they are pointed at, and targets a registered non-default topology without threading a constructor argument through. solve() does not check the resolved hash against QuantumPow.MineableTopologies. That set is the chain’s active mining set: it gates submit_proof, and so block production, not the compute mempool. An order carries its nodes, edges and coefficients inline and no topology hash at all, so the chain cannot perceive which topology an order was built against, and the solver fleet has no topology field to filter on. A topology that is registered but not mineable is proposed, matched and solved like any other.

Coefficient Encoding

The normative rules for this section are Coefficient encoding and allowed values – the same URL EncodingError and the allowed-value warning below both carry.

XQMX coefficients are natural-scale integers. The pallet stores couplings and fields as milli-scale i32 values, read back on-chain as value / 1000. Confirmed directly from xqsa/quip_codec.py:

from xqsa.quip_codec import MILLI_SCALE, MAX_NATURAL_COEFFICIENT
print(MILLI_SCALE, MAX_NATURAL_COEFFICIENT)
# 1000 2147483

SolverQuip computes spin coefficients from the model and multiplies each by MILLI_SCALE before encoding. A coefficient not exactly representable at 1/1000 precision, or one whose milli value would overflow i32, raises EncodingError rather than silently truncating.

MAX_NATURAL_COEFFICIENT is the exact ceiling only for a SPIN model, whose h/j coefficients pass through to the pallet unchanged. A BINARY model goes through a basis change first (s = 2x - 1, via dimod), which folds every quadratic coefficient into the linear field of both variables it touches: j_ij = b_ij / 4, and h_i = a_i / 2 + 1/4 * (sum of every quadratic coefficient incident on i). _to_milli() only ever sees these post-conversion values, so for a BINARY model the natural-scale ceiling moves in both directions from what the constant alone suggests:

  • An isolated coefficient survives past MAX_NATURAL_COEFFICIENT: an isolated quadratic term up to about 8_589_934 (roughly 4x), an isolated linear term up to about 4_294_967 (roughly 2x).
  • A high-degree variable can overflow well below it, because h_i aggregates a quarter of every incident quadratic term. Ten quadratic terms of 1_000_000 all touching variable 0 give h_0 = 2_500_000 – nowhere near MAX_NATURAL_COEFFICIENT – but its milli value, 2_500_000_000, exceeds i32 and raises EncodingError.

The error in that case reads h[0] = 2500000.0 overflows the encodable range, naming a linear field the caller never set directly; the actual cause is the aggregate of ten quadratic terms landing on variable 0 during the basis change. This is a range shift, not a precision loss – quarters and halves of integers are exact in floating point, and exact at 1/1000 milli scale too (0.25 becomes 250) – the stored value is correct, only the range it has to fit in moved.

Rescaling a model does not change its optimum (see Choosing a Penalty Weight), so an oversized coefficient is fixable by scaling the whole model down, not a hard ceiling on what you can express.

Each topology also declares an allowed-value set for its coefficients (the default plain-Ising spec allows {-1, 0, +1} for fields and {-1, +1} for couplings), but the pallet does not enforce it, so SolverQuip submits coefficients as-is. A miner matches jobs by an exact hash over the topology’s structure, not over the coefficient values, and real hardware rescales monotonically in any case. An out-of-spec coefficient produces a one-time warnings.warn, not a rejection.

Cost Model

Before submission, solve() reserves the configured reward (or the chain’s MinReward if none is given) from the caller’s account, in the chain’s smallest denomination, planck. A client-side balance check adds a fee-headroom buffer on top of the reward to catch an insufficient balance early with a readable error; the chain itself is the authoritative source for the actual reserve and fee deduction. If the order finalizes with no solutions, the adapter best-effort reclaims the reserved reward before raising – a failed job costs the transaction fee, not the reward. A successfully solved job’s reward is paid out on-chain to the miner; SolverQuip does not expose that payout as part of SolverResult.

This page deliberately does not give a current MinReward figure or translate planck into any external currency – see Gaps.

Metadata

SolverResult.metadata for quip carries six keys, populated from the winning submission (xqsa/quip.py:807-816):

KeyTypeMeaning
order_idintThe on-chain order id – the handle query(order_id, model) needs to recover a result after a QuipTimeoutError.
solverstr | NoneThe miner account that won the order (chosen.get("solver")); can be absent.
best_energy_milliintThe chain’s own reported best energy, in milli units.
energy_matches_chainboolCanary: whether the local milli recompute from the returned spin vector equals best_energy_milli, confirming index alignment and encoding.
num_submissionsintHow many miner submissions the order received.
num_solutionsintHow many solution vectors the winning submission carried.

best_energy_milli is milli-scale (see Coefficient Encoding) and is not directly comparable to result.energy, the authoritative natural-scale integer _recompute_energy() computes (quip.py:795); diffing the two without converting scale first will make a correct result look wrong.

metadata["solver"] here names the miner that solved the job, not a piece of hardware – D-Wave QPU uses the same key for the physical Advantage system that ran the job, so the same key names a different kind of thing depending on the backend.

energy_matches_chain is the most useful key day to day: it is the encoding-correctness canary, confirming that the placement and decoding SolverQuip used line up with what the chain itself computed.

Failure Taxonomy

Nine exception classes cover this backend, confirmed by import from xqsa.__all__ – one base class and eight concrete failures, spanning both local encoding checks and network-dependent lifecycle failures:

ExceptionRaised when
QuipErrorBase class for every error below
EncodingErrorThe model is not MODEL-mode, its domain is unsupported, or both its linear and quadratic dicts are empty. Also covers a coefficient that fails milli-scale conversion (see Coefficient Encoding)
PlacementErrorThe model’s coupling graph is not a subgraph of the target topology
QuipSigningErrorExtrinsic assembly, keystore handling, or submission fails
QuipConnectionErrorThe node is unreachable, or a configured Ising spec is not registered on-chain
QuipSubmissionErrorAn extrinsic cannot be submitted or the chain rejects it
QuipTopologyErrorRetained for compatibility; nothing raises it. It reported a topology absent from MineableTopologies, which does not gate the mempool
QuipTimeoutErrorAn order does not reach finality before the configured timeout; carries order_id so the caller can recover the result later with query()
QuipJobFailedErrorA final order has no usable solution; carries order_id

The EncodingError emptiness check tests the dicts themselves, not the values inside them (quip_codec.py:643-644) – but XQMX.set_linear() / set_quadratic() pop a key the moment its value reaches 0 (xqvm_py/xqmx.py:175-226), so for any model built through the normal API, “has no terms” and “carries only zero terms” are the same condition in practice. A model constructed directly as a dataclass, bypassing those setters, can hold explicit zero entries that leave the dicts non-empty; that model passes the EncodingError check and submits all-zero coefficient arrays.

EncodingError and PlacementError can be raised entirely locally, before anything touches the network – they come from xqsa.quip_codec, the same module the coefficient encoding above uses. The rest depend on chain state or the round trip completing.

Gaps

This chapter answers the mechanism – lifecycle, topology, encoding, cost accounting, failures – in detail. It does not answer what a reader outside the project needs to actually run a job:

  • No reader-obtainable endpoint or funding path. Nothing in this repository gives a reader a QUIP_RPC_URL or a way to fund an account. Contributors get both from an internal testing guide, which flags the coordinates as unstable and not meant to be hard-coded – which is also why they are not reproduced here.
  • No current reward or fee figures. MinReward and transaction fees are live chain state that the contributor guide itself warns changes between releases; this chapter does not assert a number that could go stale in the published book.
  • No numbers from an observed job. The propose -> solve -> decode path is exercised against a live deployment by the contributor suite, but the figures a particular submission produces – timing, an actual order_id, a real qpu_timing-equivalent, how many blocks a fresh job typically takes – are deployment- and fleet-dependent, so none is quoted here.

Energy and Precision

Every backend in this chapter returns the same kind of number, computed the same way, no matter what found the sample: an integer. This page covers how that number is computed and compared, and where precision is lost or preserved as a model moves from an exact integer formula onto real hardware.

The Formula

A sample’s energy with respect to a model is:

$$E = \sum_i \text{linear}[i] \cdot x_i ;+; \sum_{i \le j} \text{quadratic}[i,j] \cdot x_i \cdot x_j$$

identical to the model’s Hamiltonian from Quadratic Models, evaluated at one concrete assignment x, and identical to the XQVM ENERGY opcode (see Energy Evaluation). A verifier program run through XQVM and xqsa’s own recomputation compute the same formula two independent ways.

The Precision Contract

SolverResult.energy must equal compute_energy(model, sample) exactly, an integer-to-integer comparison with zero tolerance for drift. Every XQMX coefficient and every variable assignment is an integer, so the energy formula is a sum of integer products, not a float that merely rounds to the right answer. It is an exact integer whenever it is representable at all, and representable is a real condition: compute_energy checks every term product and every partial sum against the i64 range and raises ArithmeticOverflow rather than returning a wrapped number, so a model whose energy leaves that range has no energy this contract can report. Solver._recompute_energy() calls compute_energy() and casts to int; every backend in this chapter uses it instead of trusting whatever float its underlying library reports. That raw float, where one exists, survives only as metadata["params"]["raw_energy"], for diagnostics – never as the authoritative value.

A verifier program never has to take a solver’s word for its own answer. It recomputes the same integer independently with ENERGY. There are three outcomes, not two: the comparison holds, the comparison fails, or ENERGY raises ArithmeticOverflow and the verifier produces no verdict at all. The third is not a solver disagreement and must not be read as one – it says the model’s energy left the i64 range on the way to being computed. Both ENERGY and compute_energy accumulate in the same sorted key order and check the same intermediates, so the two agree on which models fall into it.

A Large Penalty Does Not Corrupt the Model

Choosing a Penalty Weight argues that a large \(P\) cannot corrupt a model, because energy differences between feasible assignments survive exactly however large \(P\) gets. Take that page’s three-node Max-Cut model with a one-hot penalty \(P \cdot (x_0 + x_2 - 1)^2\) added on top, and score two feasible assignments and one infeasible one through compute_energy() at three wildly different weights:

P=           2 | E(feasible_a)=           -8 E(feasible_b)=           -6 E(infeasible)=  -8 diff= 2
P=        1000 | E(feasible_a)=        -1006 E(feasible_b)=        -1004 E(infeasible)=  -8 diff= 2
P=  2000000000 | E(feasible_a)=  -2000000006 E(feasible_b)=  -2000000004 E(infeasible)=  -8 diff= 2

The difference between the two feasible assignments stays exactly 2 at every weight. The penalty term is 0 on both, so what separates them is the underlying objective alone, and integer arithmetic never lets it drift.

The infeasible assignment’s own energy does not move either – it sits at -8 at every \(P\) above. What moves is the two feasible energies, and they fall without bound as \(P\) grows. That is the missing +P constant, not the penalty term itself: the true, unstorable Hamiltonian adds +P to every assignment that violates the constraint and +0 to every assignment that satisfies it, so dropping that constant subtracts a uniform P from every stored energy. On the infeasible sample the missing +P exactly cancels the penalty’s own +P, leaving it unchanged; on a feasible sample there is nothing to cancel, so the stored energy falls by P.

What a Large Penalty Costs Instead

Nothing in the model degrades, but two things outside it do, and both matter to backends in this chapter.

Fixed-precision hardware loses resolution. metal-gpu computes coefficients in float32 on the GPU, not the integer arithmetic ENERGY uses – result.energy is still recomputed in exact integers afterward, but the search that finds the sample runs at float32 precision. cuda-gpu does not share this: its kernels take const double* throughout, its acceptance draws stay float64, and result.energy is recomputed the same way as every other backend. See Local Solvers for the per-backend split. The rest of this section applies to metal-gpu and to fixed-precision annealing hardware, not to every GPU backend.

A float32 mantissa carries about seven decimal digits. Once a term in the combined Hamiltonian is large enough that a difference of 2 no longer changes the float, metal-gpu’s search cannot distinguish the two assignments that difference separates, even though ENERGY still resolves them exactly afterward:

import numpy as np
np.float32(-2000000006) == np.float32(-2000000004)   # True -- the 2 vanished, at metal-gpu's width
np.float64(-2000000006) == np.float64(-2000000004)   # False -- still resolved, at cuda-gpu's width
np.float32(-1006) == np.float32(-1004)                 # False -- still resolved

At P = 2000000000 the two feasible assignments above are computationally indistinguishable to metal-gpu’s float32 search even though compute_energy() still tells them apart exactly; at P = 1000 they are not. cuda-gpu’s float64 search keeps this pair distinguishable at this scale, though a large enough \(P\) would eventually exhaust its wider but still finite mantissa too. Neither GPU backend’s correctness ever suffers – the returned energy is always the exact integer – but metal-gpu’s search can lose the signal, at penalty weights where cuda-gpu’s still keeps it visible.

Real annealing hardware rescales, and the Quip network encodes at fixed precision. A physical D-Wave annealer maps coefficients onto its analog range with a monotonic, optimum-preserving rescaling, so an oversized \(P\) is not rejected there – it costs usable range on the device instead. SolverQuip goes further: it scales every coefficient by MILLI_SCALE = 1000 into the chain’s i32 fields, so an oversized coefficient can overflow i32 and raise EncodingError rather than losing precision silently. MAX_NATURAL_COEFFICIENT = 2_147_483 is the exact bound only for a SPIN model; a BINARY model – the domain every worked example on this page uses – goes through a basis change first that shifts the bound in both directions, and can overflow well before any single coefficient looks anywhere near that large. See Quip Network for the full derivation.

A large penalty also raises the barrier between feasible regions, independent of any hardware precision question: a heuristic search such as simulated annealing has to cross that barrier to leave the first feasible region it reaches, and a larger \(P\) makes crossing less likely within a fixed number of sweeps. Sizing \(P\) in practice, and reading a solver’s output to tell which of these failure modes you hit, belongs to Constraints.

Examples

Graph problems

Examples that encode graph cuts, colouring, covers, independent sets, and tours.

  • Max-Cut – Find a 2-colour partition of a weighted graph that maximises the cut weight.
  • Graph Coloring – Assign colours to graph nodes so adjacent nodes do not share a colour.
  • Maximum Independent Set – Select the largest subset of graph nodes with no selected edge between them.
  • Vertex Cover – Select the smallest vertex subset that covers every graph edge.
  • Travelling Salesman Problem – Find the shortest Hamiltonian tour through a random symmetric distance matrix.

Selection and packing

Examples that select subsets, cover demands, pack bins, and balance integer weights.

  • Knapsack – Select items that maximise value while respecting a capacity constraint.
  • Bin Packing – Pack items into the minimum number of fixed-capacity bins.
  • Set Cover – Select the minimum set collection whose union covers the universe.
  • Weighted Set Cover – Select sets with capacities to cover element demands at minimum cost.
  • Number Partition – Split positive integers into two subsets with nearly equal sums.
  • Portfolio Optimization – Select a fixed-size portfolio while penalising higher-order risk interactions.
  • Portfolio Rebalance – Choose signed integer asset weights against a risk matrix and a budget.

Satisfiability and higher-order

Examples that reduce clauses and higher-order pseudo-Boolean objectives to quadratic models.

  • Max-3-SAT – Find the assignment that satisfies the maximum number of 3-literal clauses.
  • Cubic Optimization – Minimise a cubic pseudo-Boolean objective through HOBO degree reduction.
  • Quartic Optimization – Minimise a degree-4 pseudo-Boolean objective through two-stage REDUCE chaining.

Using the Examples

examples/ holds fourteen self-contained problems, one directory each, listed on the gallery page. This page covers what every directory has in common, how to run one, and how to turn one into a problem of your own – the task the rest of this chapter does not cover, because it is not specific to any single example.

What Every Example Shares

Each examples/<name>/ holds a README.md, which this chapter’s other pages are generated from, and a runner.py. Nothing else – no .xqasm files. problem.compile() builds the three programs in memory at runtime as plain strings; see Three Programs for what those three programs are and Compiling for what compile() emits. Every runner.py follows the same shape:

  • build_problem(...) – an xqcp Problem definition: inputs, a model, an objective, constraints, and outputs. This is the part that changes from problem to problem, and the part Modelling covers in full.
  • run(programs, ...) – three VM.run() calls against the compiled encoder, verifier, and decoder, with one xqsa solver call between the first two. Every example wires calldata and output slots the same way: encoder in, model out; model plus sample plus N in, energy and valid out; sample plus N in, decoded result out.
  • main(...) – an argparse CLI, and a JSON result printed to stdout or written with -o.

examples/manifest.yaml lists every directory once, under a group (dir, title, blurb), and drives both the gallery grouping and scripts/gen-example-docs.py’s validation that every listed directory has a runner.py and a README.md. This page itself is exempt: it is named in the manifest’s preserved_pages, so make regen-docs leaves it alone instead of overwriting it from a source README.md that does not exist.

Running One

uv run python examples/<name>/runner.py --seed 42

Every runner accepts --seed, --interpreter (python or rust, default python), --solver, and -o/--output. Most also take --n for problem size; a few split size across two flags instead (--n plus --bins, --colors, --budget, or --m), or use --num-elements and --num-sets in place of --n. Check the individual page’s Usage table for the exact flags.

--interpreter selects which XQVM runs the compiled programs: the pure-Python reference VM (xqvm_py) or the Rust interpreter through xqffi. Backends covers what else differs by solver backend; --interpreter is a different axis entirely – it picks which VM executes the programs, not which solver samples the model. A correct model produces the same valid and the same energy on both, always. The decoded result itself can differ where a model has several optima of equal energy: the solver returns one of the tied optima, not necessarily the same one on both interpreters, and a different optimum decodes to a different answer even though both are equally correct. maxcut, tsp, and knapsack happen to have no such tie at their default seed and so return identical decoded results either way; graph_coloring, bin_packing, set_cover, and max3sat do not – running examples/graph_coloring/runner.py --seed 1 gives colors: [2, 1, 2, 2, 1] on python and colors: [0, 1, 0, 0, 2] on rust, both at the same energy and both valid.

Adapting an Example

Copying and editing an existing runner.py is the fastest way to model a new problem, and it is what this section walks through concretely: turning examples/maxcut/runner.py’s Max-Cut into a minimum s-t cut with two fixed terminals.

Max-Cut splits a graph’s nodes into two groups to maximise the crossing edge weight, with no constraint on which node ends up where. Quadratic Models derives its formulation; build_problem’s inner loop is the part that matters here:

with problem.range(0, edge_count) as e:
    offset = e * 3
    i = problem.stow("i", edges_in.get(offset))
    j = problem.stow("j", edges_in.get(offset + 1))
    w = problem.stow("w", edges_in.get(offset + 2))
    problem.model.linear[i].add(-w)
    problem.model.linear[j].add(-w)
    problem.model.quadratic[i, j].add(w * 2)

Suppose two specific nodes must end up on opposite sides – node 0 and node n - 1, say, standing in for two machines a network partition must separate. That is a different problem, minimum s-t cut with fixed terminals, and it is two small edits away:

with problem.range(0, edge_count) as e:
    offset = e * 3
    i = problem.stow("i", edges_in.get(offset))
    j = problem.stow("j", edges_in.get(offset + 1))
    w = problem.stow("w", edges_in.get(offset + 2))
    problem.model.linear[i].add(w)          # sign flipped: minimise, not maximise
    problem.model.linear[j].add(w)
    problem.model.quadratic[i, j].add(w * -2)

# Fix node 0 to side 0 and node n-1 to side 1. 10_000 comfortably
# exceeds the total weight of every edge (at most C(n,2) * 100 for
# this graph), so neither fixed node is ever worth flipping to save
# weight elsewhere.
problem.model.linear[0].add(10_000)
problem.model.linear[num_nodes - 1].add(-10_000)

Everything else stays: the random weighted graph, problem.define_model, the partition output loop, and run(). The DSL calls used – model.linear[i].add() and model.quadratic[i, j].add() – are the same two maxcut/README.md already lists. main() needs one further edit, covered in the next section: drop the canonicalize_partition(partition) call, because this variant’s two sides stop being interchangeable.

Running the original and the variant on the same seed and graph (--n 5 --seed 42 on both):

Unmodified examples/maxcut/runner.py:

{"cut_weight": 354, "energy": -354, "partition": [0, 1, 0, 0, 1], "valid": 1}

The variant:

{"cut_weight": 196, "energy": -9804, "partition": [0, 1, 1, 1, 1], "valid": 1}

Both interpreters return identical output for both versions. The cut drops from 354 to 196, confirming the variant found a smaller crossing weight rather than reusing the maximiser’s answer, and partition[0] == 0 with partition[-1] == 1 confirms the two terminals landed where they were pinned.

Telling a Variant Wrong from a Variant Different

valid alone does not catch a modelling mistake here: Max-Cut and this variant both declare no constraints, so valid reports only that every sample value is 0 or 1 – see Constraints for what a verifier’s built-in check does and does not cover. energy is not a safe single number either. Get the terminal bias backwards – problem.model.linear[0].add(-10_000) and problem.model.linear[num_nodes - 1].add(10_000), the two signs swapped – and the run reports the same cut_weight (196) and the same energy (-9804) as the correct version. A cut and its complement (every node’s side flipped) always cost the same, so reversing which two nodes get pinned just picks the other member of that pair; nothing about the aggregate numbers changes.

partition is where the mistake shows, but only once canonicalize_partition(partition) is gone from main(). Max-Cut calls it because a cut and its complement are the same cut, so normalising node 0 to side 0 throws nothing away. This variant cannot afford that: the two terminals make the two sides distinguishable, and canonicalising erases the one fact the pinning added. Building both versions with that call removed and running each on the same seed and graph (--n 5 --seed 42):

Correct:

{"cut_weight": 196, "energy": -9804, "partition": [0, 1, 1, 1, 1], "valid": 1}

Swapped:

{"cut_weight": 196, "energy": -9804, "partition": [1, 0, 0, 0, 0], "valid": 1}

cut_weight and energy still agree, for the reason above. partition[0] and partition[-1] no longer do: 0 and 1 on the correct run, 1 and 0 on the swapped one. Catching this mistake means checking the property the edit was supposed to guarantee – that the two terminals sit on the sides they were pinned to – against a sample nothing downstream has normalised. The same habit generalises past this one example: name the specific property an edit is supposed to produce, and check that property against output no later step has adjusted, not just valid and a plausible-looking energy.

Max-Cut

Source: examples/maxcut/README.md

Find a 2-colour partition of a weighted graph that maximises the total weight of edges crossing the partition.

QUBO formulation

  • Input: num_nodes (int), edges (Vec of flat (i, j, w) triples, 3*|E| entries)
  • Model: n binary variables, one per node. x[v] in {0, 1} selects the side of the cut.
  • Objective: for each edge (i, j, w), add -w*(x_i + x_j) and +2w*x_i*x_j. Minimising this minimises -sum w*[x_i != x_j], i.e. maximises the cut.

DSL methods used

  • problem.input() – declare typed calldata inputs
  • problem.define_model() – allocate binary XQMX model
  • problem.stow() – bind intermediate computations to named registers
  • problem.range() – emit RANGE loops
  • model.linear[i].add() – accumulate linear bias on variable i
  • model.quadratic[i, j].add() – accumulate quadratic coupling between variables i and j
  • problem.output() – declare typed output slots
  • problem.sample.getline() – read a row from the sample bitstring

Pipeline overview

  1. CP (xqcp) – build a random weighted complete graph, declare binary variables (one per node), and add linear/quadratic QUBO terms per edge.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, then computes energy; this problem declares no constraints for it to check
  6. Decode – decoder extracts the 2-colour partition

Usage

uv run python examples/maxcut/runner.py --seed 42
uv run python examples/maxcut/runner.py --n 6 --seed 7 -o /tmp/mc.json
FlagDefaultDescription
--n5Number of nodes in the complete graph
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Graph Coloring

Source: examples/graph_coloring/README.md

Assign one of C colors to each node of an undirected graph such that no two adjacent nodes share the same color (proper C-coloring).

QUBO formulation

  • Input: number of nodes N, number of colors C, edge list
  • Model: N categorical variables over C cases. Domain.CATEGORICAL records that as an N x C binary grid, x[v,c] = 1 if node v gets color c, with a one-hot row per node.
  • Objective: none. The problem is pure constraint satisfaction: a colouring is scored only by its constraint violations. A valid C-colouring scores -penalty * N, not 0, because XQMX has no constant-term field and each satisfied ONEHOTR stores -penalty rather than 0. That is -1000 at the --n 5, penalty 200 defaults. The colour count defaults to 4 because a G(5, 0.5) graph routinely contains a 4-clique – the default seed’s does – and a 4-clique has no 3-colouring, so --colors 3 would make the canonical instance unsatisfiable. See Constraints.
  • Constraints:
    • One-hot per node: sum_c x[v,c] = 1 (ONEHOTR, penalty 200) – implied by the domain, not written
    • Exclusion per (edge, color): x[u,c] + x[v,c] <= 1 (EXCLUDE, penalty 200)

Encoding strategy

The one-hot row per node is what makes a categorical variable categorical, so define_model(domain=Domain.CATEGORICAL, k=C, penalty=200) emits it: the grid and one ONEHOTR per row, through the same record paths a hand-written loop would use. The compiled program is what this example emitted before the domain existed.

EXCLUDE is applied per (edge (u,v), color c) pair via a nested range loop. The 2D coordinates (u, c) and (v, c) are resolved to flat indices using IDXGRID: u * num_colors + c and v * num_colors + c.

Reading the answer back is sample.case(v), a ROWFIND for the 1 in node v’s row. ROWFIND returns -1 where a row holds no 1, which is the uncoloured sentinel this example used to produce in host code.

DSL methods used

  • problem.define_model(size=N, domain=Domain.CATEGORICAL, k=C, penalty=200) – the grid and its one-hot rows
  • model.apply_exclude((u, c), (v, c), penalty) – EXCLUDE per edge per color
  • sample.case(node) – the case a node took, or -1

Pipeline overview

  1. CP (xqcp) – generate a random graph, declare N categorical variables over C cases, and add EXCLUDE constraints per (edge, color) pair.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, the one-hot row sums, and the per-edge exclusions, then computes energy
  6. Decode – decoder reads each node’s case with ROWFIND, giving one color index per node

Usage

uv run python examples/graph_coloring/runner.py --seed 42
uv run python examples/graph_coloring/runner.py --n 6 --colors 4 --interpreter rust
FlagDefaultDescription
--n5Number of nodes
--colors4Number of colors
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Maximum Independent Set

Source: examples/max_independent_set/README.md

Find the largest subset of nodes in an undirected graph such that no two selected nodes share an edge.

QUBO formulation

  • Input: number of nodes N, edge list
  • Model: N binary variables. x_i = 1 if node i is in the independent set.
  • Objective: minimise -sum(x_i) (maximise set size)
  • Constraints: per edge (i,j): x_i + x_j <= 1 (SLACK + EQUALITY)

Each edge inequality is encoded via SLACK + EQUALITY. A single binary slack variable s (capacity = 1) converts x_i + x_j <= 1 into the equality x_i + x_j + s = 1, and EQUALITY adds the penalty P*(x_i + x_j + s - 1)^2.

Slack variable indices start at num_nodes and are allocated one per edge.

DSL methods used

  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • problem.slack(indices, coeffs, start_index, capacity) – append one slack entry per edge
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint

Pipeline overview

  1. CP (xqcp) – generate a random graph, declare binary variables (one per node), and encode each edge independence constraint via SLACK + EQUALITY.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that the independence constraints hold, then computes energy
  6. Decode – decoder extracts the selected nodes

Usage

uv run python examples/max_independent_set/runner.py --seed 42
uv run python examples/max_independent_set/runner.py --n 7 --interpreter rust
FlagDefaultDescription
--n5Number of nodes
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Vertex Cover

Source: examples/vertex_cover/README.md

Find the minimum subset of vertices such that every edge in an undirected graph has at least one endpoint in the subset.

QUBO formulation

  • Input: number of nodes N, edge list
  • Model: N binary variables. x_v = 1 if vertex v is in the cover.
  • Objective: minimise sum(x_v)
  • Constraints: per edge (i,j): x_i + x_j >= 1 (ATLEAST with k=1)

The at-least-1 constraint is encoded directly with ATLEAST. For each edge, ATLEAST allocates one slack variable at model.size and adds the penalty P*(x_i + x_j - 1 - s)^2, where s in {0,1} accounts for the case when both endpoints are selected (sum = 2).

DSL methods used

  • problem.vec() – allocate a vector register for the two endpoint indices per edge
  • model.apply_atleast(indices, k, penalty) – ATLEAST constraint with k=1

Pipeline overview

  1. CP (xqcp) – generate a random graph, declare binary variables (one per vertex), and encode per-edge coverage constraints via ATLEAST.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that every edge is covered, then computes energy
  6. Decode – decoder extracts the selected vertices

Usage

uv run python examples/vertex_cover/runner.py --seed 42
uv run python examples/vertex_cover/runner.py --n 7 --interpreter rust
FlagDefaultDescription
--n5Number of nodes
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Travelling Salesman Problem

Source: examples/tsp/README.md

Find the shortest Hamiltonian tour through N cities given a random symmetric distance matrix.

QUBO formulation

  • Input: num_cities (int), distance_matrix (Vec, flat upper triangle, n*(n-1)/2 entries)
  • Model: an n x n binary grid. x[i, p] = 1 means city i is at tour position p.
  • Objective: sum of distances between consecutive positions in the tour.
  • Constraints: one-hot row (each city at exactly one position) and one-hot column (each position holds exactly one city), both with penalty 100.

Hamiltonian

The Hamiltonian is the sum of three terms:

$$H = H_{\text{dist}} + H_{\text{row}} + H_{\text{col}}$$

\(H_{\text{dist}}\) is the tour length. For every pair of cities \(i < j\) and every position \(p\), a term fires whichever direction the tour visits them in – city i at p and city j at the next position, or the reverse:

$$H_{\text{dist}} = \sum_{i<j} \sum_{p=0}^{n-1} d_{ij} \left( x_{i,p}, x_{j,(p+1) \bmod n} + x_{j,p}, x_{i,(p+1) \bmod n} \right)$$

\(H_{\text{row}}\) and \(H_{\text{col}}\) are the one-hot constraints apply_onehot_row and apply_onehot_col add, at penalty \(P = 100\):

$$H_{\text{row}} = P \sum_{i=0}^{n-1} \left( \sum_{p=0}^{n-1} x_{i,p} - 1 \right)^2, \qquad H_{\text{col}} = P \sum_{p=0}^{n-1} \left( \sum_{i=0}^{n-1} x_{i,p} - 1 \right)^2$$

XQMX has no field for a constant term. Each satisfied one-hot constraint drops its +P and leaves -P in the stored model instead of 0 – see Constraints.

\(H_{\text{row}}\) forces each city onto exactly one position; \(H_{\text{col}}\) forces each position to hold exactly one city. Both terms are zero as written on a valid tour, so they never change which valid tour is shortest – but because of the dropped constant above, what a satisfied constraint actually contributes is -P, not 0, which is where the worked example’s -800 below comes from. A violation (a city visiting two positions, or a position holding two cities) still costs more than a satisfied constraint.

Whether that makes every invalid grid’s energy exceed every valid tour’s depends on \(P\) relative to the tour-length difference a violation can buy back. Constraints gives the general criterion. At \(P = 100\) it holds for this seed-42, 4-city instance. The best invalid grid scores -581, 6 above the worst valid tour’s -587. It does not hold at every instance where \(P = 100\) is used. At --n 5 --seed 7, the second command under Usage below, an invalid grid scores -770, below 40 of the 120 valid tours.

DSL methods used

  • problem.input() – declare typed calldata inputs
  • problem.define_model() – allocate binary 2D grid XQMX model
  • problem.stow() – bind intermediate computations to named registers
  • problem.range() – emit RANGE loops
  • model.quadratic[(city_i, pos), (city_j, pos)].add() – accumulate quadratic coupling using 2D grid coordinates
  • model.apply_onehot_row() – ONEHOTR constraint per city
  • model.apply_onehot_col() – ONEHOTC constraint per position
  • problem.output() – declare typed output slots
  • problem.sample.colfind() – find the row index with value 1 in a given column

Pipeline overview

  1. CP (xqcp) – build a random symmetric distance matrix, declare an n x n binary grid, and add quadratic distance terms plus one-hot row/column constraints.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and the one-hot row and column constraints, then computes energy
  6. Decode – decoder extracts the tour as a sequence of city indices

Steps 3-6 are three independent XQVM programs plus a solver call in between, sharing data only through calldata and outputs:

flowchart LR
    ENC[Encoder] -->|model| SLV[xqsa solver]
    ENC -->|model| VER[Verifier]
    SLV -->|sample| VER
    SLV -->|sample| DEC[Decoder]
    VER -->|energy, valid| OUT[Host program]
    DEC -->|tour| OUT

The encoder’s calldata is num_cities, distance_matrix; the verifier’s is model, sample, num_cities; the decoder’s is sample, num_cities. See Three Programs for why the split exists.

Worked example

uv run python examples/tsp/runner.py --n 4 --seed 42 builds this 4-city instance (edge labels are distance_matrix, indexed by the upper-triangle position of each city pair):

graph LR
    0 ---|82| 1
    0 ---|15| 2
    0 ---|95| 3
    1 ---|4| 2
    1 ---|36| 3
    2 ---|32| 3

Both interpreters return the same tour:

{
  "_note": "canonical CI golden",
  "_seed": 42,
  "energy": -650,
  "n": 4,
  "tour": [0, 2, 1, 3],
  "tour_distance": 150,
  "valid": 1
}

tour_distance is d(0,2) + d(2,1) + d(1,3) + d(3,0) = 15 + 4 + 36 + 95 = 150, matching \(H_{\text{dist}}\) for a valid tour. energy is H_{\text{dist}} + H_{\text{row}} + H_{\text{col}}: on a valid tour, each of the 4 rows and 4 columns contributes its one active variable’s -P linear term and nothing from the quadratic term (no two variables in the same row or column are both 1), for -P * n per constraint family, so energy = tour_distance - 2 * P * n = 150 - 2 * 100 * 4 = -650, matching the run.

Usage

uv run python examples/tsp/runner.py --seed 42
uv run python examples/tsp/runner.py --n 5 --seed 7 -o /tmp/tsp.json
FlagDefaultDescription
--n4Number of cities
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Knapsack

Source: examples/knapsack/README.md

The 0/1 Knapsack problem: given N items with integer weights and values, select a subset maximising total value subject to a weight capacity constraint.

QUBO formulation

  • Input: N item weights and values, capacity W
  • Model: N binary variables. x_i = 1 means item i is selected.
  • Objective: minimise -sum(v_i * x_i)
  • Constraints: capacity sum(w_i * x_i) <= W (SLACK + EQUALITY)

The inequality is encoded via SLACK + EQUALITY. SLACK appends binary slack variable entries (s_j with coefficients 2^j) to the index and coefficient vectors, converting the inequality to the equality sum(w_i*x_i) + sum(s_j*2^j) = W. EQUALITY then adds the penalty term P*(sum(a_k*x_k) - W)^2 to the QUBO.

DSL methods used

  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • problem.slack(indices, coeffs, start_index, capacity) – append slack entries
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint

Pipeline overview

  1. CP (xqcp) – generate random item weights and values, declare binary variables, and encode the capacity inequality via SLACK + EQUALITY.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that the capacity constraint holds, then computes energy
  6. Decode – decoder extracts the item selection

Usage

uv run python examples/knapsack/runner.py --seed 42
uv run python examples/knapsack/runner.py --n 6 --interpreter rust
FlagDefaultDescription
--n5Number of items
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Bin Packing

Source: examples/bin_packing/README.md

Pack N items with given integer sizes into the minimum number of bins, each with a fixed capacity C.

QUBO formulation

  • Input: N item sizes (Vec), number of bins B, bin capacity C
  • Model: (N + 1) * B binary variables in an (N + 1) x B grid. Rows 0..N-1 are the assignment cells: x[i,b] = 1 if item i is placed in bin b. Row N holds one indicator per bin: y[b] = 1 if bin b is open.
  • Objective: +BIN_COST on each indicator y[b], so the energy counts the bins the packing opens. A bias spread over the assignment cells instead would be identically N on every feasible packing, because each item lands in exactly one bin, and could not tell a one-bin packing from a three-bin one.
  • Constraints:
    • Assignment per item i: sum_b x[i,b] = 1 (ONEHOTR, penalty 200)
    • Linking per (i, b): x[i,b] -> y[b] (IMPLIES, penalty 200)
    • Capacity per bin b: sum_i s_i * x[i,b] <= C (SLACK + EQUALITY, penalty 100)

The capacity inequality is encoded by appending binary slack variable entries to the column index/coefficient vectors, converting it to a weighted equality. Each bin gets its own slack block: bin b’s entries start at (N + 1) * B + b * bitlen(C), past every model variable and past every earlier bin’s block. Sharing one start index across bins would let one bin’s slack absorb another’s overflow, leaving the capacity constraint under-constrained.

DSL methods used

  • model.apply_onehot_row(row, penalty) – ONEHOTR assignment constraint
  • model.apply_implies(coord_a, coord_b, penalty) – IMPLIES linking constraint
  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • problem.slack(indices, coeffs, start_index, capacity) – append slack entries
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint
  • xq_bitlen(value) – BITLEN, used to size each bin’s slack block

Pipeline overview

  1. CP (xqcp) – generate random item sizes, declare an (N + 1) x B binary grid, and add ONEHOTR assignment constraints per item, IMPLIES links per cell, and SLACK + EQUALITY capacity constraints per bin.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, the per-item assignment, the bin-usage links and the per-bin capacity, then computes energy
  6. Decode – decoder extracts the bin assignments

Usage

uv run python examples/bin_packing/runner.py --seed 42
uv run python examples/bin_packing/runner.py --n 5 --bins 4 --interpreter rust
FlagDefaultDescription
--n4Number of items
--bins3Number of bins
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Set Cover

Source: examples/set_cover/README.md

Given a universe of E elements and a collection of S sets, find the minimum sub-collection whose union equals the universe.

QUBO formulation

  • Input: number of elements E, number of sets S, coverage membership matrix (flat Vec of E*S entries, covers[e][s] = 1 if set s covers element e)
  • Model: S binary variables. x_s = 1 if set s is selected.
  • Objective: minimise sum(x_s)
  • Constraints: per element e: sum_{s: covers[e][s]=1} x_s >= 1 (ATLEAST with k=1)

For each element, the encoder iterates over all sets and uses a branch to conditionally push only covering set indices into the element’s index vector. ATLEAST then enforces that at least one covering set is selected.

DSL methods used

  • problem.vec() – allocate a vector register for each element’s covering set indices
  • problem.branch(cond, arm, default) – conditional VECPUSH based on coverage membership
  • model.apply_atleast(indices, k, penalty) – ATLEAST constraint with k=1

Pipeline overview

  1. CP (xqcp) – generate a random coverage matrix, declare binary variables (one per set), and encode per-element coverage constraints via conditional branching and ATLEAST.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that every element is covered, then computes energy
  6. Decode – decoder extracts the selected sets

Usage

uv run python examples/set_cover/runner.py --seed 42
uv run python examples/set_cover/runner.py --num-sets 6 --interpreter rust
FlagDefaultDescription
--num-elements4Number of elements in the universe
--num-sets5Number of sets
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Weighted Set Cover

Source: examples/weighted_set_cover/README.md

A generalisation of Set Cover where each set s has a coverage capacity cap[s] and each element e has a demand demand[e]. The goal is to select sets of minimum total cost such that the total capacity of covering selected sets meets each element’s demand.

QUBO formulation

  • Input: number of elements E, number of sets S, set costs, set capacities, element demands, coverage membership matrix
  • Model: S binary variables. x_s = 1 if set s is selected.
  • Objective: minimise sum(cost[s] * x_s)
  • Constraints: per element e: sum_{s: covers[e][s]=1} cap[s] * x_s >= demand[e] (ATLEASTW)

For each element, a branch conditionally pushes (set index, capacity) pairs into per-element index/coefficient vectors, then ATLEASTW enforces the weighted threshold.

DSL methods used

  • problem.vec() – allocate vector registers for covering set indices and capacities
  • problem.branch(cond, arm, default) – conditional VECPUSH based on coverage membership
  • model.apply_atleastw(indices, coeffs, k, penalty) – ATLEASTW constraint

Pipeline overview

  1. CP (xqcp) – generate a random weighted coverage instance, declare binary variables (one per set), and encode per-element weighted demand constraints via conditional branching and ATLEASTW.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that every demand is met, then computes energy
  6. Decode – decoder extracts the selected sets

Usage

uv run python examples/weighted_set_cover/runner.py --seed 42
uv run python examples/weighted_set_cover/runner.py --num-sets 6 --interpreter rust
FlagDefaultDescription
--num-elements4Number of elements in the universe
--num-sets5Number of sets
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Number Partition

Source: examples/number_partition/README.md

Given N positive integers, find a way to split them into two subsets of equal sum (or as close as possible if an exact split does not exist).

QUBO formulation

  • Input: N positive integers a_i
  • Model: N binary variables. x_i = 1 puts number a_i in subset A.
  • Objective: minimise P * (sum(a_i * x_i) - S/2)^2 where S = sum(a_i)

An exact partition exists when S is even and the penalty evaluates to zero. The QUBO minimiser finds the balanced partition when one exists, or the most balanced split when the total is odd.

DSL methods used

  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint

Pipeline overview

  1. CP (xqcp) – generate random positive integers, declare binary variables (one per number), and encode the half-sum equality constraint via EQUALITY.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that the partition constraint holds, then computes energy
  6. Decode – decoder extracts the subset assignment

Usage

uv run python examples/number_partition/runner.py --seed 42
uv run python examples/number_partition/runner.py --n 8 --interpreter rust
FlagDefaultDescription
--n6Number of integers
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Portfolio Optimization

Source: examples/portfolio_opt/README.md

Select a portfolio of exactly B assets from N candidates to maximise expected return while penalising higher-order risk cross-interactions.

QUBO formulation

  • Input: N asset returns, cubic risk interactions (i, j, k, sigma), budget B
  • Model: N binary variables. x_i = 1 if asset i is selected.
  • Objective: -sum(r_i * x_i) + sum(sigma_ijk * x_i * x_j * x_k) – first term maximises return (minimising its negation), second penalises correlated three-asset risk interactions.
  • Constraints: budget sum(x_i) = B (EQUALITY with unit coefficients, penalty 200)

Encoding strategy

Return terms are linear: ADDLINE(i, -r_i) per asset.

Cubic risk terms (i, j, k, sigma) are degree-reduced:

  1. REDUCE(i, j, P_AUX) -> w (Rosenberg enforcement for w = x_i * x_j)
  2. ADDQUAD(w, k, sigma) (sigma * w * x_k = sigma * x_i * x_j * x_k)

Budget constraint builds uniform-coefficient index/coeff vecs then calls EQUALITY with target = B and penalty = 200. EQUALITY is emitted after all objective (body) actions because it lands in the constraint section.

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – HOBO degree reduction for cubic risk terms
  • problem.vec() – allocate index/coefficient vecs for the budget constraint
  • model.apply_equality(indices, coeffs, target, penalty) – budget EQUALITY

Pipeline overview

  1. CP (xqcp) – generate random returns and cubic risk interactions, declare binary variables, degree-reduce risk terms via REDUCE, and add a budget EQUALITY constraint.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, that the budget constraint holds, and that each REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the selected assets

Usage

uv run python examples/portfolio_opt/runner.py --seed 42
uv run python examples/portfolio_opt/runner.py --n 6 --budget 3 --interpreter rust
FlagDefaultDescription
--n5Number of assets
--budget2Number of assets to select
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Portfolio Rebalance

Source: examples/portfolio_rebalance/README.md

Choose a signed integer weight for each asset, trading expected return off against a risk matrix, subject to the weights summing to a budget. Negative weights are short positions, which is the whole reason this example is not a binary select-or-not model.

QUBO formulation

  • Input: number of assets N, a return per asset, a symmetric N x N risk matrix flattened row-major
  • Model: N integer variables x_i in [-5, 5], declared with lo=/hi=. The model holds y_i = x_i - lo in {0, ..., 10}; coefficients are written over x and sample.value() shifts back on the way out.
  • Objective: -sum(r_i * x_i) + sum_{i <= j} C_ij * x_i * x_j
  • Constraint: sum(x_i) = B, as the penalty P * (sum_i x_i - B)^2
  • Energy: shifted twice over, and not comparable with any other example’s. The ranged domain drops the constant that substituting x = y + lo produces, because XQMX carries no offset field, and the budget square drops its own P*B^2 the way EQUALITY does. Both shifts are uniform across assignments, so argmin is exact even though the number is not the objective’s true value.

Encoding strategy

The budget constraint is written out by hand rather than handed to apply_equality. Every high-level constraint in the VM expands under x^2 = x, which holds for binary variables only, so xqcp refuses all of them off a binary model. Expanded, P * (sum_i x_i - B)^2 is P on each diagonal, 2P on each off-diagonal pair, and -2PB on each linear coefficient. Writing that square is the same work the HLF would have done, and it is what the open question about per-domain expansions is about.

The ranged domain then does its own rewriting underneath. Each quadratic write records w*lo against the linear coefficient of both named indices, because w * x_i * x_j expands to w*y_i*y_j + w*lo*y_i + w*lo*y_j + w*lo^2 once x = y + lo is substituted. A write to the diagonal lands both corrections on the one index, giving the 2*w*lo that squaring asks for. None of that is visible in the model-building code above.

lo and hi are literals. A runtime lo would want the decoder’s single calldata scalar, which the output loop bound already spends, and xqcp raises naming both rather than picking one.

DSL methods used

  • problem.define_model(size=N, domain=Domain.INTEGER, lo=-5, hi=5) – ranged integer weights
  • model.linear[i].add(w) and model.quadratic[i, j].add(w) – the only operations a non-binary model supports
  • sample.value(i) – the weight in the domain it was declared over, rather than the stored y

What this example proves

valid == 1 proves that the domain check, the record-layer shift and the decode compose on a chosen assignment. It proves nothing about optimisation: no solver samples an integer model yet, so --solver is accepted, ignored, and the pipeline runs against a hand-picked weight vector that sits inside the domain and sums to the budget. Integer lowering is XQSA v0.5.0 work.

Pipeline overview

  1. CP (xqcp) – generate returns and a symmetric risk matrix, declare N ranged integer weights, and write the objective and the budget square as coefficients
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – skipped; the runner supplies the assignment itself
  5. Verify – verifier checks every weight is inside {0, ..., 10} against the k replayed from define_model, then computes energy
  6. Decode – decoder reads each weight and adds lo, giving the signed weights back

Usage

uv run python examples/portfolio_rebalance/runner.py --seed 42
uv run python examples/portfolio_rebalance/runner.py --n 6 --interpreter rust
FlagDefaultDescription
--n5Number of assets
--solverdwave-cpuAccepted and ignored
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Max-3-SAT

Source: examples/max3sat/README.md

Given M clauses of 3 positive literals over N binary variables, find the assignment that satisfies the maximum number of clauses.

QUBO formulation

  • Input: N binary variables, M clauses of 3 positive literals
  • Model: N binary variables. x_v in {0, 1}.
  • Objective: minimise sum over clauses of P*(1-x_i)(1-x_j)(1-x_k)

A clause (i,j,k) is violated when all three variables are 0. Expanding the product (dropping the constant term):

P*(-x_i - x_j - x_k + x_i*x_j + x_i*x_k + x_j*x_k - x_i*x_j*x_k)

The cubic term -P*x_i*x_j*x_k is degree-reduced via REDUCE(i, j) -> w, introducing one auxiliary variable w per clause with Rosenberg enforcement P_AUX*(x_i*x_j - 2*x_i*w - 2*x_j*w + 3*w). The cubic term becomes the quadratic term -P*w*x_k.

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – HOBO degree reduction; returns a RegLoad holding the auxiliary variable index for chaining into quadratic terms

Pipeline overview

  1. CP (xqcp) – generate random 3-literal clauses, declare binary variables, and degree-reduce the cubic violation terms via REDUCE.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the variable assignment

Usage

uv run python examples/max3sat/runner.py --seed 42
uv run python examples/max3sat/runner.py --n 8 --m 10 --interpreter rust
FlagDefaultDescription
--n6Number of Boolean variables
--m8Number of clauses
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Cubic Optimization

Source: examples/cubic_opt/README.md

Minimise a cubic pseudo-Boolean objective over binary variables via single-stage HOBO degree reduction.

QUBO formulation

  • Input: N binary variables, M cubic interaction terms (i, j, k, c)
  • Model: N binary variables. Linear bias -1 per variable rewards selection, creating tension with the positive cubic terms.
  • Objective: sum(c_t * x_i * x_j * x_k) - sum(x_v)

Each cubic term (i, j, k, c) is degree-reduced to quadratic via:

  1. REDUCE(i, j, P_AUX) -> w – allocates auxiliary variable w with Rosenberg enforcement P_AUX*(x_i*x_j - 2*x_i*w - 2*x_j*w + 3*w)
  2. ADDQUAD(w, k, c) – adds c*w*x_k = c*x_i*x_j*x_k to the QUBO

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – single-stage HOBO degree reduction

Pipeline overview

  1. CP (xqcp) – generate random cubic interaction terms, declare binary variables with linear bias, and degree-reduce each cubic term via REDUCE.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the variable assignment

Usage

uv run python examples/cubic_opt/runner.py --seed 42
uv run python examples/cubic_opt/runner.py --n 5 --m 4 --interpreter rust
FlagDefaultDescription
--n4Number of variables
--m3Number of cubic terms
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Quartic Optimization

Source: examples/quartic_opt/README.md

Minimise a degree-4 pseudo-Boolean objective via two-stage REDUCE chaining.

QUBO formulation

  • Input: N binary variables, M quartic interaction terms (i, j, k, l, c)
  • Model: N binary variables. Linear bias -1 per variable rewards selection, creating tension with the positive quartic terms.
  • Objective: sum(c_t * x_i * x_j * x_k * x_l) - sum(x_v)

Each quartic term (i, j, k, l, c) is encoded via two-stage REDUCE:

  1. w = REDUCE(i, j, P_AUX) – introduces auxiliary w; w approximates x_i*x_j.
  2. v = REDUCE(w, k, P_AUX) – introduces auxiliary v; v approximates w*x_k = x_i*x_j*x_k. Here w is the variable index returned from the first REDUCE.
  3. ADDQUAD(v, l, c) – adds c*v*x_l = c*x_i*x_j*x_k*x_l to the QUBO.

Each quartic term allocates 2 auxiliary variables. With M terms, the model grows by 2*M variables beyond the original N.

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – two chained HOBO degree reductions; the RegLoad returned by the first REDUCE is passed as var_a to the second

Pipeline overview

  1. CP (xqcp) – generate random quartic interaction terms, declare binary variables with linear bias, and two-stage degree-reduce each quartic term via chained REDUCE.
  2. Assemble.xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the variable assignment

Usage

uv run python examples/quartic_opt/runner.py --seed 42
uv run python examples/quartic_opt/runner.py --n 6 --m 3 --interpreter rust
FlagDefaultDescription
--n5Number of variables
--m2Number of quartic terms
--solverdwave-cpuSolver backend (see Choosing a solver)
--interpreterpythonXQVM backend: python or rust
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is dwave-cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates both interpreters produce valid == 1 with --seed 42 --solver dwave-cpu. The smoke test is invariant-based – it checks validity, not exact output.

Cookbook

The fourteen examples under examples/ each solve one problem – Knapsack, TSP, Graph Coloring – and each already contains a working encoding. Nothing in the repository names those encodings as patterns you can reuse on a problem that is not itself Knapsack or TSP. This chapter names five recurring shapes instead, each grounded in the example or examples that embody it and shown as code that actually ran. One further page covers getting the arithmetic right once a shape is chosen.

This chapter assumes Modelling – it builds problems with the same xqcp calls that chapter introduces and does not re-explain them. It also assumes Quadratic Models for what a penalty term is and why one exists at all.

The Five Shapes

PageReach for it whenCanonical example
PermutationsN things need a full ordering: each thing gets exactly one position and each position gets exactly one thingexamples/tsp/
AssignmentN things each pick one of B slots, with no requirement that every slot is used or that N equals Bexamples/bin_packing/
Selection Under BudgetChoose a subset of weighted things without a total exceeding a fixed capacityexamples/knapsack/
Mutual ExclusionTwo choices conflict, or one requires anotherexamples/graph_coloring/, examples/max_independent_set/
Soft vs. Hard ConstraintsA rule that must hold sits next to a preference that should holdexamples/portfolio_opt/, examples/max3sat/

Permutations and Assignment are the same grid with one constraint family removed – see Assignment for the direction that mistake most often runs. Selection Under Budget and Mutual Exclusion overlap at one boundary: a capacity of exactly 1 on a pair is the same instruction family EXCLUDE gives you directly, covered in Mutual Exclusion. examples/bin_packing/ and examples/graph_coloring/ each compose two of these shapes in one problem (assignment plus a capacity, and assignment plus exclusion, respectively) – reading a problem as “which of these shapes does each part look like” scales to a combined problem the same way it does to a single-pattern one.

The Arithmetic Page

Integer Scaling is not a shape – every pattern above needs it, sooner or later, while turning real-valued problem data into the integer coefficients every pattern above is written in terms of. Sizing the penalty weight itself, and reading a solver’s output to tell whether a cheap constraint violation bought a good-looking energy, is Constraints’s territory, not a cookbook page of its own.

Where This Chapter Stops

None of these pages re-derives what a one-hot, EXCLUDE, or EQUALITY constraint expands to in linear and quadratic coefficients – that table is High-Level Constraints, already written, and this chapter links to it rather than repeating it. This chapter is about recognising which instruction family a new problem needs and what it costs once chosen, not about the instructions themselves.

Permutations

Assign N things to N positions so that each thing gets exactly one position and each position gets exactly one thing. Recognise it whenever “order” or “visit each once” appears in a problem statement: a tour, or a seating arrangement of N guests into N chairs.

The encoding is an \(N \times N\) binary grid, x[thing, position] = 1 meaning that thing sits at that position, with a one-hot constraint on every row and every column: ONEHOTR for “this thing takes exactly one position,” ONEHOTC for “this position holds exactly one thing.” A feasible assignment is a permutation matrix – exactly one 1 per row and per column – and the two constraint families together are what force that shape.

The Shape, Isolated

examples/tsp/ is the one example in this repository that embodies this pattern: x[city, position] = 1 means a city sits at a tour position, and the objective adds the distance between consecutive positions’ cities on top of the same row/column one-hot pair (TSP, examples/tsp/runner.py). Stripping the distance objective out leaves the pattern on its own – allocate the grid, apply both one-hot families, decode with colfind:

from xquad.cp import Domain, Problem, Types

def build_problem(n: int) -> Problem:
    problem = Problem("PurePermutation")
    num_items = problem.input("num_items", type=Types.Int)
    problem.define_model(size=num_items * num_items, domain=Domain.BINARY,
                          rows=num_items, cols=num_items)

    with problem.range(0, num_items) as row:
        problem.model.apply_onehot_row(row, penalty=10)
    with problem.range(0, num_items) as col:
        problem.model.apply_onehot_col(col, penalty=10)

    perm = problem.output("perm", type=Types.Vec)
    with problem.range(0, num_items) as position:
        perm.append(problem.sample.colfind(col=position, value=1))
    return problem

Compiling for n = 3 and running the encoder on the Rust VM, then solving the result with SolverDWaveCPU at seed=42. All six permutations of three things are tied at -60, so a different seed decodes to a different one and both lines below still hold:

decoded permutation (thing at each position): [0, 1, 2]
is a permutation of range(N): True
energy: -60

xquad verify --text accepts the compiled encoder:

ok: permutation_encoder.xqasm (29 instructions)

and xquad run --text ... --calldata 3 reproduces the same model deterministically from the assembled bytecode, independent of the Python solve above:

outputs:
  [0] = Model(XqmxModel { domain: Binary, size: 9,
    linear: {0: -20, ..., 8: -20},
    quadratic: {(0, 1): 20, (0, 2): 20, ..., (7, 8): 20}, rows: 3, cols: 3 })

Nine linear terms at -20 (one per cell: each cell belongs to one row and one column, so it picks up -penalty twice) and eighteen quadratic terms at +20 (every same-row and every same-column pair) – exactly ONEHOTR’s and ONEHOTC’s expansions from High-Level Constraints superimposed on the same grid. examples/tsp/ builds the identical row/column structure and adds distance coefficients on top; nothing about the permutation half changes when an objective joins it.

Cost in Variables

The grid is \(N^2\) variables – no slack, no auxiliary variables, since both ONEHOTR and ONEHOTC only rewrite coefficients on cells that already exist. That quadratic growth is the real cost of this pattern: doubling the number of things to order quadruples the variable count, before an objective term is added.

Failure Mode

Applying only one axis – ONEHOTR without ONEHOTC, or the reverse – does not raise an error; it produces a model where each thing still picks exactly one position, but nothing stops two things from picking the same one. That looser rule is Assignment. The other failure is structural rather than semantic: ONEHOTR/ONEHOTC read grid dimensions from RESIZE, so a model with no grid set has no row or column for them to constrain. Both raise InvalidGridDimensions at the instruction that needed the grid, on either interpreter – see High-Level Constraints. xquad verify does not catch it, since grid dimensions are runtime values, so the failure surfaces when the encoder runs rather than when it compiles.

Assignment

Match each of N things to exactly one of B slots, with no requirement that every slot receives a thing and no requirement that B equals N. Recognise it whenever one set is matched into another with no requirement that the match run both ways: items into bins, nodes onto colours.

This is Permutations with the column half removed. Encode it as an \(N \times B\) grid, x[thing, slot] = 1 meaning that thing occupies that slot, with a one-hot family on every row only: each thing picks exactly one slot, but a slot may hold zero, one, or many things. Dropping the column constraint gives a different rule, not a weaker form of the same one, and Permutations names the mistake in the other direction: applying only one axis where both are wanted.

Recognising the Row-Only Shape

examples/bin_packing/ assigns each item to exactly one bin – one ONEHOTR per item row of its grid (Bin Packing, examples/bin_packing/runner.py):

# Assignment constraint: each item i must go in exactly one bin
with problem.range(0, num_items) as i:
    problem.model.apply_onehot_row(i, 200)

ONEHOTR is the special case of EQUALITY with \(a_k = 1\) and \(b = 1\), which High-Level Constraints states directly. Writing it by hand – a vec() pair per row, an index and a 1 pushed per column, then apply_equality(indices, coeffs, 1, 200) – expresses the same constraint and costs a loop and two vectors per row. The model here is defined with both rows and cols set, so apply_onehot_row applies and the hand-rolled form buys nothing.

Bin packing’s grid has one row more than it has items. Rows 0..N-1 are the assignment cells; the extra row N holds one indicator variable per bin, and apply_implies((i, b), (num_items, b), 200) opens bin b as soon as any item lands in it. That indicator row is what lets the objective count bins: a bias spread over the assignment cells would sum to N on every feasible packing, because each item lands in exactly one bin, so it could not tell a one-bin packing from a three-bin one.

Running examples/bin_packing/runner.py --seed 42 (4 items, 3 bins, capacity 5, sizes [1, 2, 1, 1]) decodes to assignment: [2, 2, 2, 2] – all four items in bin 2, total size 5 against a capacity of 5. The rust interpreter returns [0, 0, 0, 0] instead: which single bin gets used is a tie, and the two interpreters break it differently. Every item appears in exactly one bin, which is what the row constraint guarantees; nothing requires a bin to be used, and the bin-count objective pushes the other way.

Bin packing composes this row-assignment pattern with a second, unrelated one – each bin’s contents must not exceed its capacity, encoded with SLACK plus EQUALITY exactly like the capacity constraint in Selection Under Budget. That capacity half is not this page’s concern; see Selection Under Budget for the pattern on its own.

Cost in Variables

The grid costs \(N \times B\) variables against Permutations’ \(N^2\), and the saving is entirely the grid: it is cheaper exactly when there are fewer slots than things (\(B < N\)). Dropping the column constraint costs nothing and saves nothing in variables, since neither one-hot family allocates any – EQUALITY with in-range indices does not grow model.size on its own (High-Level Constraints).

Failure Mode

The one this page warns about runs the other direction from Permutations: adding a column one-hot to an assignment problem that does not want one silently turns “N things into B slots” into “a bijection between things and slots,” which only has a feasible solution at all when \(N = B\). If a Permutations-shaped model with RESIZEd grid dimensions comes back consistently infeasible, check whether the problem actually needs both axes constrained or only one.

Selection Under Budget

Choose a subset of N things, each with a cost, so the total cost does not exceed a fixed capacity, while maximising (or minimising) something else about the chosen subset. Recognise it whenever a problem says “at most,” “no more than,” or “fits within” about a sum of per-item weights: a knapsack, a budget of assets, a shipment under a weight limit.

An inequality (<=) combined with a per-item weight that is not always 1 is what distinguishes this shape from Mutual Exclusion’s <= on a pair, or a plain one-hot’s = 1 – here the bound is a general capacity and the items carry arbitrary integer weights. None of ONEHOTR, ONEHOTC, EXCLUDE, or IMPLIES express an inequality; ATLEAST and ATLEASTW cover the mirrored >= direction directly (Vertex Cover, Weighted Set Cover), but XQVM has no <= counterpart over an arbitrary weighted sum. SLACK bridges the gap, turning the inequality into an equality that EQUALITY can enforce – see Constraints for why that substitution is exact, and SLACK for the instruction itself. model.apply_inequality(indices, coeffs, target, capacity, penalty) composes both calls into one – see Constraints, including the caveat there that the target parameter is not a target value: it is where slack variables begin, normally the count of real variables, and capacity is the actual bound. This page assumes both and covers only the recognition and the cost.

The Canonical Instance

examples/knapsack/ is this pattern with nothing else mixed in – Knapsack, examples/knapsack/runner.py. Calling its own build_problem with a fresh instance (six items, capacity 12) rather than the seed-42 instance the Modelling chapter already worked through:

from examples.knapsack.runner import build_problem
from xquad.vm import VM, VMBackend

n = 6
weights = [2, 3, 4, 5, 6, 7]
values = [3, 4, 5, 8, 9, 10]
capacity = 12

problem = build_problem(n, weights, values, capacity)
programs = problem.compile()

vm = VM(backend=VMBackend.RUST)
vm.set_calldata([n, weights, values, capacity])
vm.set_output_slots(1)
vm.run(programs.encoder)
model = vm.outputs()[0]

model.size comes back 10: six item variables plus four slack bits, S = floor(log2(12)) + 1 = 4. Solving with SolverDWaveCPU and decoding:

selection [0, 0, 0, 1, 0, 1] weight 12 capacity 12 feasible True value 18 energy -14418
verifier energy -14418 valid 1

Items 3 and 5 (weights 5 and 7, values 8 and 10), weight exactly at capacity, value 18. Brute-forcing all 64 subsets confirms 18 is the true optimum, not just a feasible value: 28 of the 64 fit within capacity, and no other reaches value 18. xquad verify on the compiled encoder (43 instructions) passes.

Cost in Variables

\(N\) item variables plus \(S = \lfloor \log_2(\text{capacity}) \rfloor + 1\) slack bits – logarithmic in the capacity, not the capacity itself, which is what makes representing “at most a million units” cost twenty extra variables rather than a million. Integer Scaling covers a consequence of this formula worth knowing before choosing how finely to scale a fractional capacity: the slack count grows with the scaled capacity, so a needlessly large scale factor costs real variables, not just larger coefficients.

examples/bin_packing/ repeats this exact SLACK + EQUALITY shape once per bin, for a per-bin capacity rather than a single global one – see Assignment, which covers the rest of that problem’s structure. Its start_index argument to SLACK advances per bin – model_vars + b * xq_bitlen(capacity) – so each bin’s capacity constraint reaches for its own slack block. Passing the same fixed start index on every iteration is the trap when copying this block as a per-bin template: the bins then share slack variables, and one bin’s overflow can be absorbed by another’s slack. The capacity-constraint shape itself, in isolation, is the same one this page covers.

Failure Mode

A SLACK/EQUALITY pair that looks right can still be infeasible for a reason that has nothing to do with the penalty weight: if every item’s weight already exceeds capacity, or the cheapest single item does, no slack combination rescues it, since slack only adds toward the target, never subtracts from the real items’ contribution (SLACK). Check the raw numbers – smallest item weight against capacity – before reaching for penalty-weight guidance to explain a solver that keeps returning something that looks wrong.

Mutual Exclusion

Two choices conflict, and a solution may take at most one of them. Recognise it in “not both,” “at most one,” or an adjacency rule that forbids a shared property between connected things: two colours on adjacent nodes, two overlapping bookings.

EXCLUDE is the direct opcode: pop penalty, then two variable indices, and add quad[i, j] += penalty – a pure coupling term, no linear part, that costs penalty exactly when both variables are 1 and nothing otherwise (High-Level Constraints). This page also covers IMPLIES, the directional relative: “picking i requires j” rather than “picking both is forbidden.”

EXCLUDE, Direct

examples/graph_coloring/ applies EXCLUDE once per edge per colour: two adjacent nodes may not both hold the same colour (Graph Coloring, examples/graph_coloring/runner.py):

with problem.range(0, num_edges) as e:
    offset = problem.stow("offset", e * 2)
    u = problem.stow("u", edges_in.get(offset))
    v = problem.stow("v", edges_in.get(offset + 1))
    with problem.range(0, num_colors_in) as c:
        problem.model.apply_exclude((u, c), (v, c), 200)

Running examples/graph_coloring/runner.py --seed 1 --interpreter rust (5 nodes, 4 colours, 6 edges) returns colors: [0, 1, 0, 0, 2], is_valid: true, energy: -1000 – every one of the six edges checked by hand connects two nodes with different colours.

Forcing the default --seed 42 instance down to --colors 3 shows what EXCLUDE can and cannot do. That seed’s random edges form a 4-clique among nodes {0, 2, 3, 4}, every pair among them an edge, and a 4-clique has no 3-colouring at all. The run reports is_valid: false, energy: -800, and valid: 0; no adjustment to EXCLUDE’s penalty weight changes that. EXCLUDE enforces a rule, and it can only enforce a colouring that exists. The colour count defaults to 4 for exactly this reason. Note that the generated verifier’s valid agrees with the runner’s own is_valid here: it checks each EXCLUDE against the sample directly.

The Same Rule, Encoded Without EXCLUDE

examples/max_independent_set/ needs the identical x_i + x_j <= 1 rule per edge – two adjacent nodes cannot both be in the set – but builds it with SLACK plus EQUALITY instead of EXCLUDE (Max Independent Set):

edge_indices = problem.vec()
edge_coeffs = problem.vec()
edge_indices.push(ni)
edge_indices.push(nj)
edge_coeffs.push(1)
edge_coeffs.push(1)
problem.slack(edge_indices, edge_coeffs, num_nodes + e, 1)
problem.model.apply_equality(edge_indices, edge_coeffs, 1, 200)

Both encode the same inequality; EXCLUDE is a one-instruction convenience for exactly the pairwise case, while SLACK + EQUALITY costs one extra slack variable per edge to reach the identical rule (Selection Under Budget’s pattern, applied here to a capacity of 1). Reach for EXCLUDE whenever the conflict is pairwise, which it almost always is; reach for the manual form only when the exclusion needs to compose with other terms already riding the same index/coefficient vectors.

Running examples/max_independent_set/runner.py --seed 42 (same 5-node, 6-edge graph as the graph-coloring default above, node set {0, 2, 3, 4} forming the clique) returns in_set: [0, 1, 0, 0, 1] – nodes {1, 4}, is_independent: true. Running examples/vertex_cover/runner.py --seed 42 against the identical graph returns cover: [1, 0, 1, 1, 0] – nodes {0, 2, 3}, the exact complement of {1, 4} in {0, 1, 2, 3, 4}. That is not a coincidence of this one instance: a set of nodes covers every edge exactly when the nodes left out share no edge, so a minimum vertex cover and a maximum independent set on the same graph are always complements of each other. But vertex_cover’s own constraint, x_i + x_j >= 1 via ATLEAST, is not this page’s pattern – it requires at least one endpoint, the opposite direction from forbidding both. Do not reach for EXCLUDE when a problem says “at least one of,” even on the same graph shape that motivates exclusion elsewhere.

IMPLIES

No example in this repository calls apply_implies. A minimal demonstration, run rather than assumed, checks the sign directly:

from xquad.cp import Domain, Problem, Types
from xquad.vm import VM, VMBackend

problem = Problem("ImpliesDemo")
n = problem.input("n", type=Types.Int)
problem.define_model(size=n, domain=Domain.BINARY)
problem.model.apply_implies(0, 1, 50)   # picking 0 requires picking 1
problem.model.linear[0].add(-30)        # a reason to pick 0 at all

programs = problem.compile()
vm = VM(backend=VMBackend.RUST)
vm.set_calldata([2])
vm.set_output_slots(1)
vm.run(programs.encoder)
model = vm.outputs()[0]

Reading the resulting coefficients back and evaluating all four assignments by hand:

linear: {0: 20}, quadratic[0,1]: -50
x0=0 x1=0 -> H=0
x0=0 x1=1 -> H=0
x0=1 x1=0 -> H=20
x0=1 x1=1 -> H=-30

x0=1, x1=0 – picking 0 without 1, the forbidden combination – costs 20 more than leaving both off, and exactly 50 more than the legal x0=1, x1=1, matching the penalty=50 passed in. The -30 reward on x0 alone is what gives the solver a reason to pick it in the first place; without it, x0=0 would dominate trivially and the implication would never be tested. IMPLIES’ own expansion, linear[i] += penalty, quad[i, j] += -penalty (High-Level Constraints), is exactly what produced 20 and -50 here: linear[0] = -30 + 50 = 20, no change to linear[1], quadratic[0,1] = 0 - 50 = -50.

Cost in Variables

EXCLUDE and IMPLIES add no variables at all – both rewrite an existing coefficient pair. The SLACK-based encoding of the same rule costs one extra slack bit per pair, since a capacity of 1 needs \(S = \lfloor \log_2(1) \rfloor + 1 = 1\) bit (SLACK).

Failure Mode

A graph that needs more colours than it is given, as the graph-coloring seed-42 default above shows, is not a penalty-tuning problem: no penalty value turns an infeasible instance feasible, since the penalty only controls how costly a violation is, not whether one is avoidable. If EXCLUDE or IMPLIES constraints keep the best returned sample stuck at a nonzero violation at more than one penalty weight, check whether the underlying combinatorial structure allows a solution to exist before reaching for penalty-weight guidance.

Soft vs. Hard Constraints

Every rule in a quadratic model is a penalty term, but not every penalty term means the same thing to the problem it belongs to. A hard constraint is a rule the solution must obey. It is satisfied or it is violated, with nothing in between, and the only question a penalty weight answers is how expensive violating it becomes. A soft constraint – a preference is the more precise name – is a cost a solution pays continuously: more of the disfavoured thing costs more, but nothing marks any amount as forbidden. apply_equality, apply_onehot_row, apply_exclude, and the rest of the constraint family from Constraints are how a hard rule is written. A preference is written directly into the objective, the way Objectives covers, with no constraint call at all.

Both in One Problem

examples/portfolio_opt/ carries one of each. The budget is hard – sum(x_i) = B via apply_equality, unconditional, penalty 200 – and the risk term is soft – a cubic penalty added straight into the objective for every asset triple whose combination the problem considers risky, with no accompanying constraint call (Portfolio Optimization, examples/portfolio_opt/runner.py):

# Cubic risk cross-terms via REDUCE
with problem.range(0, num_risk) as t:
    ...
    w = problem.model.reduce(ti, tj, _P_AUX)
    problem.model.quadratic[w, tk].add(sigma)

# Budget constraint: sum(x_i) = B  (EQUALITY with unit coefficients)
...
problem.model.apply_equality(indices, coeffs, budget_in, 200)

reduce and ADDQUAD build the risk cost the same way any other objective term is built – see Objectives and High-Level Constraints for REDUCE’s own Rosenberg terms, which enforce the auxiliary variable’s algebraic identity, not the risk rule itself. Nothing about sigma is a threshold; it is a coefficient like any other, weighing a return against a risk directly in the same sum.

The Discriminating Difference: A Threshold vs. a Continuous Trade-off

Fix four assets with returns [10, 9, 8, 7], a budget of 3, and one risky triple (0, 1, 2, sigma), then sweep sigma. run_case below covers the encoder half – compiling the problem, running the encoder to build the model, and solving it – the same way the earlier snippets on this page do. Decoding the returned sample back into a portfolio runs programs.decoder over [sample, n], exactly as examples/portfolio_opt/runner.py does it. That step is elided here because it is the same on both sides of the comparison:

from examples.portfolio_opt.runner import build_problem
from xquad.vm import VM, VMBackend
from xqsa import build_solver

def run_case(sigma):
    risk_terms = [(0, 1, 2, sigma)]
    problem = build_problem(4, [10, 9, 8, 7], 3, risk_terms)
    programs = problem.compile()
    flat_risk = [v for term in risk_terms for v in term]
    vm = VM(backend=VMBackend.RUST)
    vm.set_calldata([4, [10, 9, 8, 7], 3, 1, flat_risk])
    vm.set_output_slots(1)
    vm.run(programs.encoder)
    model = vm.outputs()[0]
    result = build_solver("dwave-cpu", seed=42).solve(model, num_reads=200)
    ...  # decode via programs.decoder and print, as below

Decoded and printed, sigma=1 and sigma=2 give:

sigma=1: portfolio=[1, 1, 1, 0] total_return=27 energy=-1826
sigma=2: portfolio=[1, 1, 0, 1] total_return=26 energy=-1826

Across twenty solver seeds at each sigma (seed in range(1, 21)), sigma=2 always switches to {0, 1, 3} (return 26), stable on every seed. sigma=1 does not behave the same way: it sits exactly at the tie, so the sampler returns either portfolio, split close to evenly across seeds – {0, 1, 2} on 12 of 20 and {0, 1, 3} on 8 of 20, both at the identical energy -1826. The tie is exact, not approximate: asset 2’s risk cost only applies when all three of 0, 1, 2 are selected together (REDUCE’s product term), so choosing the risky trio costs -27 + sigma against the safer trio’s fixed -26; the two costs are equal at sigma = 1 and the risky choice stops paying for itself once sigma > 1, which is exactly where the stable switch to {0, 1, 3} sets in. A weight that lands exactly on a tie is not evidence either portfolio is preferred – it is evidence the weight is at the boundary, and a single sample there does not tell you which side you are on. Every selection in this sweep still has exactly three assets – the hard budget constraint held at every sigma, unaffected – while which three assets the soft risk cost favoured changed at its break-even point, with a tie rather than a clean switch exactly at that point. That contrast is what the two kinds of constraint buy: a hard rule’s own penalty weight only decides whether the rule can be broken at all (Quadratic Models, Constraints), while a soft term’s weight decides how much of the preference the solution actually buys.

Soft, With No Hard Constraint at All

examples/max3sat/ sits at the other end: every clause is a preference, none is a rule the solution must satisfy, and the problem carries no apply_* call anywhere (Max-3-SAT). A clause (i, j, k) costs P_CLAUSE exactly when all three of its literals are false, added directly to the objective; a solution that satisfies zero clauses is legal, just expensive. Running examples/max3sat/runner.py --seed 42 (6 variables, 8 clauses) satisfies all eight (satisfied: 8, energy: -80) for this random instance, but nothing in the model would reject a sample that satisfied fewer – there is no valid check tied to clause satisfaction, because there is no constraint to check.

The valid flag draws exactly this line. A hard constraint is an apply_* call, and the generated verifier emits one check per call: portfolio_opt’s budget is an apply_equality, so a sample selecting all four assets against a budget of 3 comes back valid: 0. Max-3-SAT’s clauses are objective terms, so nothing in its verifier looks at them and a sample satisfying two clauses is as valid as one satisfying eight. If a rule must hold, declare it as a constraint; Verification says what valid then covers.

Cost in Variables

Neither pattern taxes the model directly – a soft term is ordinary linear/quadratic coefficient writes, and a hard constraint’s cost is whatever its own instruction adds (ATLEAST/ATLEASTW/REDUCE grow model.size; EXCLUDE/IMPLIES/EQUALITY-with-in-range-indices do not). portfolio_opt’s risk terms cost one REDUCE auxiliary variable each, so the model grows from n to n + num_risk, independent of the hard budget constraint sharing the same model.

Failure Mode

Writing a preference as a hard constraint at a large penalty forces an all-or-nothing rule where a graded cost was wanted – the model will never choose to pay a little of the disfavoured thing even when the rest of the objective would gain more from it, because the constraint’s cost jumps from 0 to penalty * d^2 the instant it is touched at all (Constraints). The reverse mistake, writing a rule that must always hold as a soft objective term, is worse: nothing stops the solver from breaking it whenever the rest of the objective offers enough of a reward, and there is no way to check afterward that it did not, the way max3sat’s missing valid check above demonstrates directly.

Integer Scaling

Every XQMX coefficient is an i64, and every arithmetic operation on one is exact – Quadratic Models and Energy and Precision both rest on that fact. Real problem data rarely arrives as integers: prices, weights in kg, fractional returns. This page covers turning that data into i64 coefficients without losing precision the problem needs, and what a scale factor costs once it is chosen. No example in this repository has fractional inputs to scale, so everything below is a fresh, executed worked instance rather than a citation.

Choosing the Scale Factor

Multiply every fractional value by a factor large enough that the result is exactly an integer, and use that integer as the coefficient. The factor has to be large enough for every value in the problem, not just the ones that look round. A four-item knapsack with quarter-kilogram weights and cent-precision values:

weights_f = [2.50, 3.75, 1.25, 4.00]
values_f = [10.20, 15.75, 8.40, 20.00]
capacity_f = 7.50

def scale(values, factor):
    scaled = [round(v * factor) for v in values]
    for v, s in zip(values, scaled):
        assert abs(v * factor - s) < 1e-9, (v, factor, s)   # catch silent rounding
    return scaled

Scaling by 4 – enough for the weights, which are all quarters – fails the assertion the moment it reaches the values: 10.2 * 4 = 40.8 is not integral (rounds to 41), so scale() raises before the demo builds a model from this factor:

AssertionError: (10.2, 4, 41)

The values are cent-precision but not all quarters (10.20, 8.40 are not multiples of 0.25), so 4 is not enough. The minimal factor that is exact for every value here is the least common multiple of every value’s decimal denominator – 20 (every one of these numbers is a multiple of 0.05), not the naive 100 a “two decimal places” guess would reach for:

from fractions import Fraction
from math import lcm
denoms = [Fraction(v).limit_denominator(10000).denominator
          for v in weights_f + values_f + [capacity_f]]
lcm(*denoms)   # 20

At factor = 20: weights = [50, 75, 25, 80], values = [204, 315, 168, 400], capacity = 150. Building this through xqcp (the same SLACK + EQUALITY shape Selection Under Budget covers, at the penalty weight of 4 derived in Coefficient Magnitude below), compiling, running the encoder on the Rust VM, solving with SolverDWaveCPU, and dividing the decoded totals back by the scale factor:

selection=[1, 1, 1, 0] total_weight=7.5 total_value=34.35 capacity_ok=True energy=-90687

7.5 and 34.35 are exact – not rounded back, computed by an integer sum divided by 20 with no remainder, since 20 was chosen to be exact for every input. xquad verify accepts the compiled encoder (43 instructions), and the same selection, {0, 1, 2}, comes back identically across five different solver seeds.

What Scaling Costs: Slack Bits

SLACK’s bit count is \(S = \lfloor \log_2(\text{capacity}) \rfloor + 1\) (SLACK) – logarithmic in the capacity, but the capacity that matters is the scaled one. The naive factor = 100 scaling of the same instance needs 10 slack bits (capacity = 750); the minimal exact factor = 20 needs 8 (capacity = 150). Scaling five times more finely than the data requires costs two extra variables here, and the gap widens as the capacity grows – an unnecessarily large scale factor is not free, even before its effect on coefficient magnitude below.

What Scaling Costs: Coefficient Magnitude, and Two Backends That Disagree

A penalty weight multiplies every constraint coefficient (High-Level Constraints), so a scale factor and a penalty compound. Using the loose safe penalty Constraints gives as a fallback – one more than the sum of absolute linear coefficients, 1088 for the values above, since the rule is penalty greater than that sum – against the factor = 20 model:

loose_penalty=1088: max |coefficient| = 23,953,408
fits MAX_NATURAL_COEFFICIENT (2,147,483): False

Quip Network confirms the exact bound directly from xqsa.quip_codec:

from xqsa.quip_codec import MILLI_SCALE, MAX_NATURAL_COEFFICIENT
print(MILLI_SCALE, MAX_NATURAL_COEFFICIENT)
# 1000 2147483

The loose bound, safe on its own terms, overflows SolverQuip’s milli-scale i32 encoding more than tenfold at this scale factor. Enumerating this specific instance the way Constraints enumerates its own – sixteen subsets, tightest violator {0, 2, 3} at weight 155, value gap 85 over an excess of 5 – gives a tight threshold of 85 / 5^2 = 3.4, so the smallest safe integer penalty is 4, not 1088:

tight penalty=4: max |coefficient| = 88,064
fits MAX_NATURAL_COEFFICIENT: True

4 clears the tight threshold and the solver still returns the correct optimum, {0, 1, 2}, identically across five seeds – the two hundred seventy-two-fold gap between the loose and tight penalty is exactly what made the difference between overflowing SolverQuip’s encoding and fitting it comfortably. This is Constraints’s own warning about the loose bound being loose, made concrete by a scale factor large enough to expose it: a bound that is merely safe at natural scale can become the deciding factor once a scale factor multiplies every coefficient it touches.

The same coefficient growth affects metal-gpu’s float32 search differently from cuda-gpu’s float64 one, the way Energy and Precision covers for an unscaled model. At the loose penalty above, the resulting solved energy (-24,480,687, order \(10^7\)) is past float32’s roughly seven decimal digits of resolution for a difference of 1, but not for a difference of 100:

import numpy as np
e = -24480687   # this model's actual solved energy at the loose penalty
np.float32(e) != np.float32(e - 1)     # False -- the 1-unit difference is lost
np.float32(e) != np.float32(e - 100)   # True -- a 100-unit difference still resolves

A scaling choice that looks safe by natural-scale reasoning can cost metal-gpu resolution cuda-gpu keeps, at the identical model – check the actual coefficient magnitudes your factor produces against the target backend, not just against the input data’s precision.

Cost in Variables

\(S = \lfloor \log_2(\text{scaled capacity}) \rfloor + 1\) slack bits on top of the item count, same formula as Selection Under Budget, with the scaled capacity – not the original – as the input.

Failure Mode

Picking a scale factor from “how many decimal places does this look like” rather than the actual least common multiple of every value’s denominator either drops precision silently (a factor too small, caught here only because the demo asserts the rounding was exact) or costs variables and coefficient headroom for no reason (a factor larger than any value needs, the 100 vs. 20 case above). Compute the minimal exact factor once, from every value the problem uses, rather than guessing a round number.

XQVM Reference

XQVM is the virtual machine at the core of XQuad: a stack-based interpreter with a 256-slot register file that executes compiled quadratic-optimisation programs. This section is the reference for that machine – its execution model, assembly language, instruction set, binary format, and verifier. For what a QUBO/Ising model is and why you would want one, start at Concepts instead; this section assumes you already have a program, or want to write one directly in .xqasm, and want to know exactly what the machine does with it.

A minimal program

Save this as add.xqasm, then assemble and run it:

; push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
$ xquad asm add.xqasm -o add.xqb
assembled 4 instructions (21 bytes) -> add.xqb
$ xquad run add.xqb
stack (bottom to top):
  42

The program pushes 10 and 32 onto the value stack, adds them, and halts. The result remains on the stack and is printed by xquad run. xquad run --text add.xqasm skips the separate assembly step and runs the source directly.

What this section covers

VM Architecture

XQVM is a stack-based bytecode interpreter. A running VM holds four pieces of mutable state:

StateHoldsNotes
Stacki64 valuesLIFO operand stack, max 8,192 items
Register FileRegVal values256 slots, indexed r0–r255
Loop Stackloop framesone per active RANGE/ITER; see Loops
Calldata / OutputsRegVal valuesread-only inputs (INPUT) and writable output slots (OUTPUT); see Calldata and Outputs

Design Principles

  • Stack-based computation – arithmetic and comparisons operate on an integer stack. This keeps the instruction set simple and compact.
  • Typed register file – registers hold polymorphic RegVal values (integers, vectors, models, samples). Type checking happens at runtime.
  • No heap / no pointers – there is no explicit memory allocation. Vectors and models grow dynamically within registers. Programs cannot address raw memory.
  • Deterministic execution – given the same program, calldata, and configuration, the VM always produces the same output. There are no random instructions or non-deterministic operations.
  • Embeddable – the VM crate supports no_std + alloc, enabling deployment in WASM runtimes and bare-metal environments.

Operand Stack

The operand stack is the primary workspace for computation. It holds i64 signed 64-bit integers and is used by arithmetic, comparison, logical, and bitwise instructions.

Properties

PropertyValue
Element typei64 (signed 64-bit integer)
Maximum depth8,192 items
OrderingLIFO (last in, first out)
Initial stateEmpty

Operations

  • PushPUSH1PUSH8 push constants. LOAD pushes a register’s integer value. COPY duplicates the top element.
  • Pop – most instructions implicitly pop their operands. POP explicitly discards the top element.
  • SwapSWAP exchanges the top two elements.
  • ClearSCLR removes all elements.

Stack Diagrams

Throughout this documentation, stack effects are written as:

$$[\ldots, a, b] \to [\ldots, r]$$

  • \(\ldots\) represents elements below the operands.
  • Rightmost = top of stack.
  • \(b\) is popped first (it was pushed last).
  • \(r\) is the result pushed after the operation.

Errors

  • StackUnderflow – popping from an empty stack or when there are fewer elements than the instruction requires.
  • StackOverflow – pushing when the stack already contains 8,192 items.

Interaction with Registers

The stack holds only i64 integers. Richer types (models, vectors, samples) live exclusively in registers. The bridge between them:

  • LOAD reg – pushes a register’s Int value onto the stack.
  • STOW reg – pops a stack value into a register as Int.

To move non-integer values, use INPUT/OUTPUT with calldata and output slots.

Register File

The register file is a fixed array of 256 slots, indexed r0 through r255. Each slot holds a typed RegVal value.

Properties

PropertyValue
Count256 (r0–r255)
Index typeu8
Value typeRegVal (polymorphic enum)
Default valueUnset for all slots

RegVal Variants

VariantRust TypeDescription
UnsetDefault. No value; a register never written, or reset by DROP.
Int(i64)i64Exchanged with the stack via LOAD/STOW.
VecInt(Vec<i64>)Vec<i64>Integer vector. Created by VEC/VECI.
VecXqmx(Vec<XqmxModel>)Vec<XqmxModel>Vector of models. Created by VECX.
Model(XqmxModel)structQUBO/Ising/integer Hamiltonian. Created by BQMX/SQMX/XQMX.
Sample(XqmxSample)structVariable-assignment vector. Created by BSMX/SSMX/XSMX.

Type Checking

Register access is type-checked at runtime. Instructions that expect a specific variant (e.g. LOAD expects Int, VECPUSH expects VecInt, SETQUAD expects Model, and SETLINE accepts either Model or Sample) will produce a RegisterType error if the register holds a variant the instruction does not accept. The error message includes the expected and actual type names. Reading an Unset register (via LOAD or OUTPUT) is a separate case: it produces an UnsetRegister error rather than RegisterType, since there is no variant to compare against.

XqmxModel Structure

A model represents a QUBO/Ising/integer Hamiltonian:

XqmxModel {
    domain: Domain,                      // Binary | Spin | Integer(k)
    size: usize,                         // number of variables
    linear: BTreeMap<usize, i64>,        // bias terms h_i
    quadratic: BTreeMap<(usize,usize), i64>,  // coupling terms J_{ij}
    rows: usize,                         // grid rows (set by RESIZE)
    cols: usize,                         // grid cols (set by RESIZE)
}

Coefficients are stored sparsely. Missing entries read as 0; setting a coefficient to 0 removes it from the map.

XqmxSample Structure

A sample holds a vector of variable assignments:

XqmxSample {
    domain: Domain,        // must match the model's domain
    values: Vec<i64>,      // one value per variable
    rows: usize,           // grid rows (set by RESIZE; 0 if ungridded)
    cols: usize,           // grid cols (set by RESIZE; 0 if ungridded)
}

Memory Management

There is no garbage collector. Registers hold their values until explicitly overwritten. Use DROP reg to reset a register to Unset, releasing any heap allocation (models, vectors, samples) it held. A register reset this way faults with UnsetRegister on the next LOAD or OUTPUT, until something is written back into it.

Loop Stack

The loop stack manages RANGE and ITER loop state. Each active loop pushes a frame; NEXT either advances the loop or pops the frame when iteration completes.

Loop Frames

Each frame records:

  • KindRange or Iter.
  • body_start – byte offset of the first instruction after RANGE/ITER. This is where NEXT seeks back to on each iteration.

Range Loops

A range frame tracks two values:

  • current – the current iteration value.
  • end – the exclusive upper bound (start + count).

RANGE pops count and start from the stack. The loop iterates current from start to end - 1 (where end = start + count). On each NEXT, current is incremented. If current < end, execution seeks back to body_start; otherwise the frame is popped and execution falls through. If count is zero or negative, no frame is pushed at all: execution scans forward past the matching NEXT and the body never runs.

PUSH 5       ; start = 5
PUSH 3       ; count = 3
RANGE        ; iterates current = 5, 6, 7
  LVAL r0    ; r0 ← Int(current)
  ; ... body ...
NEXT

Iterator Loops

An iterator frame tracks three values:

  • elements – a copy of the slice vec[start_idx..end_idx], holding either integers or models depending on the source register’s variant.
  • start_offset – the original start_idx, used by LIDX to report absolute positions.
  • index – the current position within elements.

ITER reg pops end_idx, then start_idx, validates that reg holds VecInt or VecXqmx, and copies vec[start_idx..end_idx] into a new frame with index = 0. The slice is duplicated so that mutations to the source vec inside the loop body do not affect what LVAL sees.

On each NEXT, index is incremented. If index is still within the copied elements, execution seeks back to body_start; otherwise the frame is popped.

; Assume r1 holds VecInt([10, 20, 30, 40, 50])
PUSH 1
PUSH 4
ITER r1            ; iterate r1[1..4] -> values 20, 30, 40
  LVAL r2          ; r2 -> Int(20), Int(30), Int(40)
  LIDX r3          ; r3 -> Int(1), Int(2), Int(3) (absolute positions)
  ; ... body ...
NEXT

ITER errors with IndexOutOfBounds if either index is negative or exceeds vec.len().

An empty slice skips the body, exactly like RANGE with a count of zero: no frame is pushed and execution resumes after the matching NEXT. The condition is start_idx >= end_idx, so an inverted range is empty rather than an error, and a slice that is never taken is not bounds-checked.

LVAL – Reading the Loop Value

LVAL reg copies the current loop value into a register:

  • Range: reg ← Int(current)
  • Iter over VecInt: reg ← Int(elements[index]) (the slice copy, not the source vec)
  • Iter over VecXqmx: reg ← Model(elements[index]) (cloned)

The element type is preserved: iterating over a VecXqmx yields Model values, not integers. Because elements is a slice copy taken at ITER time, mutating the source vec inside the loop body never changes what subsequent LVAL calls return.

LIDX – Reading the Loop Index

LIDX reg copies the current loop index into a register:

  • Range: reg ← Int(current) (identical to LVAL because Range values are themselves indices).
  • Iter: reg ← Int(start_offset + index) – the absolute position in the source vec, not the 0-based slice position. This lets loop bodies reach back into the source vec by absolute index even after slicing.

Nesting

Each RANGE or ITER pushes a new frame, and LVAL and NEXT always operate on the innermost (most recently pushed) frame. The loop stack is capped at 8,192 frames, the same cap the value stack has carried since the first release; a program past it fails with LoopStackOverflow. Real nesting never approaches that. The cap exists because only NEXT pops a frame, so a back-edge that re-enters a loop header without running its NEXT grows the stack once per execution and would otherwise be bounded only by the step budget.

PUSH 0
PUSH 3
RANGE              ; outer loop: 0, 1, 2
  LVAL r0
  PUSH 0
  PUSH 4
  RANGE            ; inner loop: 0, 1, 2, 3
    LVAL r1
    ; r0 = outer value, r1 = inner value
  NEXT
NEXT

Errors

  • NoActiveLoopNEXT, LVAL, or LIDX with an empty loop stack.
  • RegisterTypeITER on a register that is not VecInt or VecXqmx. Raised before the empty-slice check, so an ITER on the wrong register type faults whether the slice is empty or not.
  • LoopStackOverflowRANGE or ITER pushing a frame past the 8,192-frame cap.
  • IndexOutOfBoundsITER with a slice index outside the vector.
  • ArithmeticOverflowRANGE whose start + count leaves the i64 range. The bound is computed before the frame is pushed, so the loop does not run at all.

Calldata and Outputs

Calldata and output slots provide the interface between the VM and the host environment. They allow programs to receive input and return results without direct access to external systems.

Calldata (Input)

Calldata is a read-only array of RegVal values, set before execution begins. Programs access calldata via the INPUT instruction:

PUSH 0       ; slot index
INPUT r0     ; r0 ← calldata[0]

Any RegVal variant can be passed as calldata: integers, vectors, models, and samples. This enables multi-program pipelines where one program’s output model becomes another program’s input.

Setting Calldata (Rust API)

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_calldata(vec![
    RegVal::Int(42),
    RegVal::VecInt(vec![1, 2, 3]),
    RegVal::Model(my_model),
]);
}

Setting Calldata (CLI)

xquad run program.xqb --calldata 10,20,30

The CLI --calldata flag only supports integer values. For richer types, use the Rust API.

Output Slots

Output slots are a writable array of RegVal values, initialised to Unset (the same default a fresh register holds). Programs write to output slots via the OUTPUT instruction:

PUSH 0       ; slot index
OUTPUT r0    ; outputs[0] ← r0

Reading Outputs (Rust API)

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_output_slots(4);
vm.run(&program)?;

for (i, val) in vm.outputs().iter().enumerate() {
    println!("[{i}] = {val:?}");
}
}

Reading Outputs (CLI)

xquad run prints all non-default (not Unset) output slots after execution. --outputs defaults to 16 when omitted:

xquad run program.xqb --outputs 4
outputs:
  [0] = Int(42)

Pipeline Pattern

Calldata and outputs enable multi-program pipelines. A common pattern in the TSP example:

  1. Encoder receives N and distances as calldata, outputs a QUBO model.
  2. Verifier receives the model and a sample as calldata, outputs energy and validity.
  3. Decoder receives the sample as calldata, outputs the tour.

Each program runs in its own Vm instance. The host (Rust code or pallet) marshals outputs from one VM into calldata for the next.

Errors

  • CallDataIndexINPUT with an index ≥ calldata length.
  • OutputIndexOUTPUT with an index ≥ output slot count.
  • UnsetRegisterOUTPUT from a register that was never written, or was reset with DROP.

Execution Model

This chapter describes how the VM fetches, decodes, and executes instructions.

Fetch-Decode-Execute Cycle

The VM processes instructions in a loop:

1. Fetch and decode the next instruction from the instruction stream
   - end of stream → stop execution, without consulting the step budget
2. Check the step budget → StepLimitExceeded if the counter already
   reached the limit; the fetched instruction does not execute
3. Increment the step counter
4. Dispatch to the handler for that instruction
5. Handle the control flow result:
   - Continue   → advance to next instruction
   - Halt       → stop execution
   - Jump(id)   → seek to the byte offset recorded for that TARGET id
   - Seek(off)  → seek to byte offset (used by NEXT)
   - StartLoop  → a loop frame was pushed; continue to the next instruction
   - SkipLoop   → loop count was zero or negative; scan past the matching NEXT
6. Repeat from step 1

The fetch comes before the budget check, not after, and that ordering is normative rather than an implementation detail – see Step budget in the specification. The fetch that finds no instruction ends the run before the budget is consulted, so it is not a step.

Instruction Stream

The instruction stream is a cursor over the program’s raw bytecode. It decodes one instruction at a time, advancing the cursor past the opcode byte and its operands. The stream supports seeking to arbitrary byte offsets for jumps and loop backs.

Before execution begins, the raw bytecode is scanned once for TARGET opcodes: each is assigned the next sequential id (0, 1, 2, …) in program order, and its byte offset is recorded against that id. A Jump result carries one of these ids; the run loop resolves it to the recorded offset and seeks the stream there.

Each decoded instruction yields:

  • Byte offset – position in the bytecode buffer.
  • Optional label – the sequential TARGET id recorded at this offset, if any.
  • Instruction – the fully decoded instruction with typed operands.

Step Counting

The VM maintains two counters, not one. instructions counts dispatches: it increments once per instruction, regardless of what that instruction does. steps counts metered cost units: every instruction charges a base cost before dispatch, and opcodes whose work scales with data the program controls – evaluating a model, expanding a constraint, copying a register that holds a model – charge more before they do that work. A step is not an instruction; see spec/xqvm/METERING.md for the full cost model. A configurable step limit (default: 10,000,000) prevents runaway programs by bounding steps, not instructions. When a charge does not fit the remaining budget, execution stops with a StepLimitExceeded error.

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_step_limit(1_000_000);  // custom limit
vm.set_step_limit(0);          // exact: permits no instructions at all
vm.set_unlimited_steps();      // the only unbounded spelling
}

The limit is exact. Until 0.4.0 set_step_limit(0) meant u64::MAX, which made a zero budget the most dangerous value a caller could pass rather than the safest – the wrong way round for anything taking a limit from untrusted input. There is no sentinel now: set_unlimited_steps() is how a caller opts out, and it has to be written.

Both counters are accessible after execution: vm.steps() returns the metered cost total, vm.instructions() returns the dispatch count. instructions() reports the exact number of instructions dispatched, whether the program ends at a HALT or by running off the end of the instruction stream: the loop probes for the next instruction before it counts anything, so the fetch that finds nothing and breaks the loop is never counted. xqvm_py’s executor has the same loop shape, so the two interpreters report the same count for the same program.

One exception is worth knowing: when a loop opener skips an empty body, the scan forward to the matching NEXT charges steps for each instruction it consumes but does not dispatch them, so those instructions are metered without being counted in instructions().

Allocation Accounting

Steps bound how long a program runs, not how much memory it asks for: a three-instruction program can name a sample of any size the value stack can hold. A second counter tracks bytes. Every allocating instruction is charged against a configurable budget (default: 1 GiB) before it allocates, and one that cannot pay stops execution with a MemoryLimitExceeded error without allocating anything.

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_memory_limit(16 * 1024 * 1024);  // 16 MiB
}

vm.memory_used() reports what the run spent. See Runtime Limits for the per-opcode charges.

Control Flow Results

Each instruction handler returns a StepResult that tells the execution loop what to do next:

ResultMeaning
ContinueAdvance to the next instruction in sequence.
HaltStop execution. Returned by HALT.
Jump(label)Seek the instruction stream to the byte offset recorded for that TARGET id.
Seek(offset)Seek to a raw byte offset. Used by NEXT to loop back.
StartLoopA loop frame was pushed; continue to the next instruction (which becomes the loop body start).
SkipLoopThe loop count was zero or negative; scan forward past the matching NEXT without pushing a frame.

Tracing

The VM supports optional step-by-step tracing via the Tracer trait. When tracing is enabled, the VM captures state before and after each instruction:

#![allow(unused)]
fn main() {
pub struct StepState<'a> {
    pub pos: usize,                     // byte offset
    pub step: u64,                      // step count
    pub instruction: &'a Instruction,   // decoded instruction
    pub stack: &'a [i64],               // current stack
    pub read_regs: &'a [(u8, RegVal)],  // registers read
    pub written_regs: &'a [(u8, RegVal)], // registers written
    pub loop_depth: usize,              // nesting level
}
}

Two built-in tracer implementations are provided:

  • TextTracer – human-readable aligned columns, written to any Write target.
  • JsonTracer – one JSON object per step (JSONL format).

When tracing is disabled (NoopTracer), the tracer code is eliminated by dead code optimisation, adding zero overhead to execution.

Error Handling

Runtime errors carry the byte offset (pos) of the faulting instruction, enabling precise error reporting. When the std feature is enabled, errors can be converted to miette::Diagnostic with a disassembled listing highlighting the faulting instruction.

Assembly Language

XQVM programs are written in a simple assembly language and stored in .xqasm files. The assembler (the xqasm crate, invoked via xquad asm) parses the source, resolves labels, and emits compact bytecode.

Overview

  • Line-oriented format: one instruction per line.
  • Comments start with ; and run to end of line.
  • Mnemonics are case-insensitive (PUSH, push, Push all work).
  • Labels use numeric .N syntax (.0, .1, .42).
  • Registers use r<digits> syntax (r0, r255).
  • Integer literals may be signed decimal or 0x-prefixed hexadecimal.

Quick Example

; Compute 10 + 32 = 42
PUSH 10
PUSH 32
ADD
HALT

Assembly Syntax

This page defines the complete syntax of the XQVM assembly language, derived from the canonical PEG grammar in xqasm/src/grammar.pest.

Line Structure

Each source line has the form:

[label_def:] [INSTRUCTION [operands...]] [; comment]

All three parts are optional. Blank lines and comment-only lines are valid.

Examples

                        ; blank line (valid)
; this is a comment     ; comment-only line
PUSH 42                 ; instruction only
.0: NOP                 ; label + instruction
.1:                     ; label only (anchors a jump target)
LOAD r0                 ; register operand
JUMP .0                 ; label reference operand

Comments

Comments begin with ; and extend to the end of the line. They can appear on their own or after an instruction:

; full-line comment
PUSH 10  ; inline comment

Mnemonics

Instruction mnemonics are case-insensitive ASCII identifiers. All of these are equivalent:

PUSH 42
push 42
Push 42

The assembler recognises 85 of the 93 XQVM mnemonics directly. The eight PUSH1PUSH8 opcodes are not typeable: PUSH is a special mnemonic that accepts an integer operand and selects the smallest of those encodings for you, and writing one of them by hand is rejected as an unknown mnemonic. PUSHC is an alias for PUSH. JUMP and JUMPI are the same kind of sugar, but layered on top of opcodes that are themselves among the 85: JUMP1, JUMPI1, JUMP2, JUMPI2 are part of the 93 and can be written directly, though doing so interacts badly with the unused-label check – see Control Flow. JUMP and JUMPI are two further mnemonics, not among the 93, that pick the narrowest width for you the same way PUSH picks a PUSHn width. See Stack Manipulation for the width rules.

Operands

Three operand types exist:

Registers

r0, r1, r2, ..., r255

A lowercase r followed by 1–3 decimal digits. Valid range: r0r255.

Integer Literals

42          ; positive decimal
-99         ; negative decimal
+7          ; explicit positive
0xFF        ; hexadecimal (0x prefix)
0x0         ; hex zero

Integers are signed i64 values. Decimal and hexadecimal (0x prefix) formats are supported. An optional + or - sign may precede the digits.

Label References

.0, .1, .42, .255

A dot followed by one or more decimal digits. Label references are used as operands for the JUMP/JUMPI mnemonics (and their explicit-width forms JUMP1/JUMPI1/JUMP2/JUMPI2).

Labels

Labels are defined either with the .N: shorthand or the explicit TARGET .N directive:

.0: NOP            ; shorthand: define label .0 at this position
.1:                ; label on its own line (useful for readability)

TARGET .2          ; explicit form: identical to ".2:"
HALT

Both forms compile to the same bytecode: placing a label emits an inline TARGET opcode at the current position and records that position under the label’s assigned id. .0: and TARGET .0 are interchangeable spellings for the same operation; pick whichever reads better in context. Defining the same label with both forms is a DuplicateLabel error, the same as defining .N: twice.

A bare TARGET (no operand) emits a raw Target opcode without binding any label. That’s useful only for hand-built bytecode where you do not need a corresponding jump destination; user-facing programs should use the labelled forms.

Labels must be defined before or after they are referenced – both forward and backward references are resolved by the assembler. Every label used as a JUMP/JUMPI target must be defined somewhere in the program.

The .N digits are assembler-only syntax: they let the assembler pair a JUMP/JUMPI reference with the .N: (or TARGET .N) that defines it, and they are never emitted into the bytecode. What reaches the instruction stream is only a sequence of bare TARGET opcodes. See Bytecode Format for how a decoder assigns those opcodes their sequential ids and resolves a jump operand against them.

Whitespace

Spaces and tabs between tokens are ignored. Lines are separated by \n or \r\n. Indentation is purely cosmetic and has no semantic meaning. A common convention is to indent loop bodies:

PUSH 0
PUSH 10
RANGE
  LVAL r0
  LOAD r0
  PUSH 2
  MUL
  POP
NEXT

Error Reporting

The assembler uses miette for rich terminal diagnostics. Errors include the source file name, line/column numbers, and a snippet highlighting the problematic token:

Error: xqasm::unknown_mnemonic

  × unknown mnemonic `BADOP`
   ╭─[bad.xqasm:2:1]
 1 │ PUSH 1
 2 │ BADOP r0
   · ──┬──
   ·   ╰── unknown mnemonic
 3 │ HALT
   ╰────

xquad asm shows the same diagnostics from the command line.

Assembly Examples

This page presents annotated XQVM assembly programs, from simple to complex.

Hello, Stack

Push two numbers, add them, and halt. The result remains on the stack.

PUSH 10        ; stack: [10]
PUSH 32        ; stack: [10, 32]
ADD            ; stack: [42]
HALT

Conditional Branch

Skip an instruction if a condition is true:

PUSH 5
PUSH 10
GT             ; 5 > 10 ? -> 0 (false)
JUMPI .0       ; condition is 0, so we fall through
PUSH 99        ; this executes (condition was false)
JUMP .1
.0: PUSH 0     ; taken path leaves the stack at the same depth
.1: HALT

Summing a Range

Accumulate the values of a range loop into a register:

PUSH 0         ; accumulator in r0
STOW r0

PUSH 0
PUSH 3
RANGE          ; iterate 0, 1, 2
  LVAL r1      ; r1 = loop value
  LOAD r0
  LOAD r1
  ADD
  STOW r0      ; r0 += r1
NEXT

LOAD r0        ; push accumulated value (0+1+2 = 3)
HALT

Fibonacci Sequence

Compute the first N Fibonacci numbers and store them in a vector:

; N is passed as calldata[0]
PUSH 0
INPUT r0       ; r0 = N

VEC r1         ; r1 = empty vec

; Push first two values
PUSH 0
VECPUSH r1     ; vec = [0]
PUSH 1
VECPUSH r1     ; vec = [0, 1]

; Compute remaining values
LOAD r0
PUSH 2
SUB            ; count = N - 2
STOW r2

PUSH 0
LOAD r2
RANGE
  LVAL r3      ; r3 = loop index (unused, just for iteration)
  VECLEN r1
  DEC
  STOW r4      ; r4 = last index

  LOAD r4
  DEC
  VECGET r1    ; stack: fib[n-2]
  LOAD r4
  VECGET r1    ; stack: fib[n-2], fib[n-1]
  ADD           ; stack: fib[n]
  VECPUSH r1   ; append to vec
NEXT

; Output the vector
PUSH 0
OUTPUT r1
HALT

Building a QUBO Model

Create a simple 3-variable QUBO and set coefficients:

; Allocate a 3-variable binary model
PUSH 3
BQMX r0

; Set linear coefficients: h = [-1, -2, -3]
PUSH 0
PUSH -1
SETLINE r0     ; linear[0] = -1

PUSH 1
PUSH -2
SETLINE r0     ; linear[1] = -2

PUSH 2
PUSH -3
SETLINE r0     ; linear[2] = -3

; Set quadratic coefficient: J[0,1] = 4
PUSH 0
PUSH 1
PUSH 4
SETQUAD r0     ; quad[0,1] = 4

; Output the model
PUSH 0
OUTPUT r0
HALT

Grid with One-Hot Constraints

Set up a 2x3 grid model with one-hot constraints on each row:

; 6 variables in a 2x3 grid
PUSH 6
BQMX r0
PUSH 2         ; rows
PUSH 3         ; cols
RESIZE r0

; One-hot constraint on each row with penalty = 100
PUSH 0
PUSH 2
RANGE
  LVAL r1
  LOAD r1
  PUSH 100
  ONEHOTR r0
NEXT

; Output
PUSH 0
OUTPUT r0
HALT

Instruction Set Reference

This section documents all 93 XQVM instructions, organised into the 14 categories below. Each page covers the semantics, error conditions, and worked examples for one category. For the opcode byte, mnemonic, operand layout, and stack effect of every instruction, see the generated Opcode Reference – that table is the single source of truth for wire-level facts, so the pages in this section carry prose and teaching content instead of repeating it.

Notation

  • Stack effect – described in prose on this page (“Pop b, then a”) and summarised on the generated Opcode Reference as a Stack column of the form 2 → 1, meaning two values popped and one pushed. The value stack is last-in-first-out: whichever value was pushed most recently is popped first, so PUSH a; PUSH b; SUB computes a - b, not b - a. Every “pop x, then y” operand order on this section’s pages, including the ones on constraints.md and index-math.md, depends on this rule.
  • reg – the u8 operand encoded in the instruction byte stream, identifying a register slot (r0–r255).
  • label – a sequential id assigned to each TARGET instruction, in program order, during the load-time pre-scan described in Bytecode Format. It is not a byte offset, and not the .N token used in .xqasm source – that token is assembler-only and is resolved to the sequential id before encoding. JUMP1/JUMPI1 encode the id as a single u8 operand; JUMP2/JUMPI2 encode it as a u16 operand (big-endian). See Control Flow for how the assembler picks between the two.
  • Assignments use \(\leftarrow\) (register write) and \(\to\) (stack push).
  • Iverson brackets – \([P]\) equals \(1\) if \(P\) is true, \(0\) otherwise.
  • Checked arithmetic – every integer operation is checked against the i64 range. A result that would leave it raises ArithmeticOverflow rather than wrapping or panicking; see Arithmetic.

Register Effect Vocabulary

A handful of pages, constraints.md and energy.md, annotate individual opcodes with a **Register effect:** line using three terms:

  • read – register contents are inspected but not changed.
  • write – register is replaced wholesale with a new value.
  • mutate – register’s existing value is modified in-place (e.g. appending to a vec, incrementing a coefficient).

Most category pages group opcodes by topic or mechanism rather than walking through them one at a time, so they carry no such line.

RegVal – The Register Value Type

Each of the 256 registers holds one variant of RegValUnset, Int, VecInt, VecXqmx, Model, or Sample. See VM Architecture for the full table.

Type mismatches at runtime produce a RegisterType error with the expected and actual variant names; reading an Unset register via LOAD or OUTPUT produces UnsetRegister instead, since there is no variant to compare against.

Categories

CategoryPageOpcodes
Control Flowcontrol-flow.mdTARGET, JUMP1, JUMP2, JUMPI1, JUMPI2, NEXT, LVAL, LIDX, RANGE, ITER, NOP, HALT
Register I/Oregister-io.mdLOAD, STOW, DROP, INPUT, OUTPUT
Stack Manipulationstack-manipulation.mdPOP, PUSH1-PUSH8, SCLR, SWAP, COPY
Arithmeticarithmetic.mdADD, SUB, MUL, DIV, MOD, SQR, ABS, NEG, MIN, MAX, INC, DEC, BITLEN
Comparisoncomparison.mdEQ, LT, GT, LTE, GTE
Logical Booleanlogical.mdNOT, AND, OR, XOR
Bitwisebitwise.mdBAND, BOR, BXOR, BNOT, SHL, SHR
Allocatorsallocators.mdBQMX, SQMX, XQMX, BSMX, SSMX, XSMX, VEC, VECI, VECX
Vector Operationsvector-ops.mdVECPUSH, VECGET, VECSET, VECLEN, SLACK
Index Mathindex-math.mdIDXGRID, IDXTRIU
Coefficient Accesscoefficient-access.mdGETLINE, SETLINE, ADDLINE, GETQUAD, SETQUAD, ADDQUAD
Grid Operationsgrid.mdRESIZE, ROWFIND, COLFIND, ROWSUM, COLSUM
High-Level Constraintsconstraints.mdONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, EQUALITY, ATLEAST, ATLEASTW, REDUCE
Energy Evaluationenergy.mdENERGY

Reserved Opcodes

Every byte value not listed in Opcode Reference is illegal; the decoder rejects it. The 93 assigned bytes are not contiguous – there are gaps both between and within the ranges above, plus one large unassigned block above the normal instruction space. The full gap set, derived by diffing the 93 assigned byte values against the complete 0x00-0xFF space:

RangeUnassigned bytes
Register I/O0x0D
Stack Manipulation0x19, 0x1D-0x1F
Arithmetic0x2D-0x2F
Comparison0x35
Allocators0x46-0x49, 0x4D-0x4F
Vector Operations0x55-0x59
Index Math / Coefficient Access0x5C-0x5F
Grid Operations0x6B-0x6F
High-Level Constraints0x78-0x7E
Outside the normal instruction space0x80-0xEF, 0xF1-0xFE

NOP (0xF0) and HALT (0xFF) are the only two assigned bytes outside 0x00-0x7F; every other byte in 0x80-0xFF is illegal. See spec/xqvm/ISA.md for the normative list, organised the same way.

Control Flow

Instructions for branching, looping, and program termination: TARGET, JUMP1/JUMP2, JUMPI1/JUMPI2, NEXT, LVAL, LIDX, RANGE, ITER, NOP, and HALT. For each instruction’s opcode byte, operand layout, and stack effect, see the Opcode Reference.

The TARGET pre-scan

TARGET has no operand and does nothing at runtime – it exists purely to mark a valid jump destination at a fixed byte position. Before execution begins, the VM scans the raw instruction stream once for TARGET opcodes: the first one encountered is assigned sequential id 0, the second id 1, and so on in program order, with each id recorded against the byte offset where its TARGET starts. This is the same scan the verifier uses to check that every jump references a valid id, and the Bytecode Format chapter covers its wire-level details.

JUMP1, JUMP2, JUMPI1, and JUMPI2 operands carry one of these sequential ids – never a raw byte offset, and never the .N token that appears in .xqasm source. .N is resolved to the sequential id by the assembler before encoding and is never itself emitted into the bytecode. When the VM executes a jump, it looks up the id’s recorded byte offset and seeks the instruction stream there; see Execution Model for how this fits into the fetch-decode-execute loop.

TARGET must appear at every label destination. The assembler inserts one automatically wherever a label is placed in source, so authors do not normally type it by hand:

; Shorthand: label form
.0: HALT
; Explicit form: TARGET directive bound to a label
TARGET .0
HALT

Both compile to [TARGET, HALT]. Use whichever is clearer in context. A bare TARGET (with no operand) emits a raw Target opcode without binding any label; that is only useful for direct bytecode construction, and most user programs should prefer one of the label-bearing forms above.

Branching

JUMP unconditionally seeks to the byte offset recorded for a label. JUMPI pops the top of the stack and seeks only if that value is non-zero; otherwise it falls through to the next instruction.

Each has a narrow and a wide encoding, distinguished by the width of the label operand:

  • JUMP1 / JUMPI1 encode the label as a single u8 byte.
  • JUMP2 / JUMPI2 encode the label as a u16 big-endian pair.

At the assembly level, both are written as JUMP .N / JUMPI .N (.N being the dot-prefixed label token, e.g. .0, .1); the assembler resolves .N to its sequential id and picks the narrowest encoding automatically – the *1 form whenever the id fits in a u8 (the common case, since most programs have fewer than 256 labels), falling back to the *2 form otherwise. The desugared forms (JUMP1 .N, JUMP2 .N, JUMPI1 .N, JUMPI2 .N) are accepted by the grammar, but the assembler’s unused-label check does not count them as a use: a program whose only reference to .0 is JUMP1 .0 fails with xqasm::unused_label, whichever side of the label the jump sits on. The one exception is a label whose TARGET lands at byte offset 0, the entry block, which the check exempts. Write JUMP/JUMPI and let the assembler pick the width. Disassembled output always shows the explicit form the program was actually encoded with, so a round-tripped disassembly preserves the exact wire encoding rather than the assembler’s shorthand.

Looping

RANGE and ITER each push a loop frame and hand control to the loop body; NEXT advances the innermost frame, seeking back to the body’s start until the loop is exhausted, then pops the frame. LVAL copies the current loop value into a register, and LIDX copies the current loop index. Calling LVAL, LIDX, or NEXT with no active loop frame raises NoActiveLoop.

PUSH 0       ; start
PUSH 10      ; count
RANGE
  LVAL r0    ; r0 = current iteration value (0, 1, ..., 9)
NEXT
HALT
VECI r1
PUSH 10
VECPUSH r1
PUSH 20
VECPUSH r1
PUSH 30
VECPUSH r1
PUSH 40
VECPUSH r1

PUSH 0       ; start_idx
PUSH 4       ; end_idx
ITER r1      ; r1 must hold a VecInt or VecXqmx
  LVAL r2    ; r2 = current element (10, 20, 30, 40)
  LIDX r3    ; r3 = absolute position in r1 (0, 1, 2, 3)
NEXT
HALT

Loops nest to arbitrary depth; LVAL, LIDX, and NEXT always act on the innermost frame. Loops covers frame contents, the RANGE versus ITER distinction, nesting, and the full error conditions in depth – this page only orients; that one is the reference.

NOP and HALT

NOP does nothing and advances to the next instruction; it exists mainly for hand-assembled bytecode and testing. HALT stops execution immediately, leaving the stack and registers as they were at the point of the halt.

Register I/O

Instructions for moving data between the value stack, the 256-slot register file, calldata, and output slots: LOAD, STOW, DROP, INPUT, OUTPUT. Byte values, operand layouts and stack effects are in the Register I/O section of the opcode reference.

Registers Default to Unset, Not Int(0)

Every register starts as RegVal::Unset, not Int(0). Only LOAD and OUTPUT fault UnsetRegister when reading an unset register; every other instruction that reads a register instead reports RegisterType, with unset as the actual variant held, for example register r7 holds unset, expected vec<int> from VECPUSH r7, register r7 holds unset, expected model|sample from GETLINE r7, or register r7 holds unset, expected sample from ENERGY r0 r7. LOAD r3 as the first instruction of a program, before anything has written to r3, fails at runtime with register r3 is unset. The verifier’s uninitialised-register check catches the same defect statically for straight-line code, one reason to run xquad verify before xquad run: the sequence PUSH 1 / STOW r0 / DROP r0 / LOAD r0 is rejected by verify with register r0 read at byte 0x0006 before being written, before the runtime fault is ever reached.

LOAD distinguishes two failure modes for a register that is not usable as an integer. An unset register faults UnsetRegister; a register holding something other than Int, for example a Model from BQMX, faults RegisterType instead, naming the actual variant held. LOAD on a register that holds a freshly allocated Model fails with register r0 holds model, expected int, a different message from the unset case.

Stack-Register Bridge

The value stack holds only i64 integers. LOAD pushes a register’s Int value onto the stack; STOW pops the stack top and writes it into a register as Int. Neither coerces: LOAD on a non-Int register errors rather than reinterpreting the value as an integer.

Richer types, models, vectors and samples, never touch the stack directly. Moving them into or out of the VM goes through INPUT and OUTPUT instead, against the calldata and output-slot arrays rather than the stack. INPUT reg pops a calldata slot index and clones calldata[slot] into reg; OUTPUT reg pops an output slot index and clones reg’s current value into outputs[slot]. Either direction transfers any RegVal variant, not only Int, since calldata and outputs are typed as Vec<RegVal> on the Rust side; the xquad CLI’s --calldata flag only accepts a comma-separated integer list, so injecting a Model or Sample through calldata is something host code building on the xqvm/xqasm crates can do, not something the CLI exposes directly. PUSH 0; INPUT r0; PUSH 0; OUTPUT r0, given --calldata 77, round-trips a value through a register with no arithmetic in between: output slot 0 ends up holding Int(77). Both INPUT and OUTPUT fault with CallDataIndex/OutputIndex respectively when the popped slot index is out of range, and OUTPUT faults UnsetRegister if reg was never written, the same as LOAD.

Memory Management

DROP reg is the only instruction that explicitly frees a register’s allocation, resetting it to Unset and releasing whatever Model, Sample, VecInt or VecXqmx the slot held. It does not leave an integer zero behind: the register is unreadable until the next STOW, INPUT or allocator call.

Stack Manipulation

Instructions for pushing constants and rearranging the top of the value stack: POP, PUSH1..PUSH8, SCLR, SWAP and COPY. Byte values, operand layouts and stack effects are in the Stack Manipulation section of the opcode reference. This page is mostly about the constant-pushing family, since the four rearrangement instructions (POP, SCLR, SWAP, COPY) are exactly what their names say: discard the top, clear everything, exchange the top two, or duplicate the top without consuming it. SWAP and COPY both error StackUnderflow rather than reading past an empty stack: a bare PUSH 1; SWAP needs a second element that is not there.

PUSH Size Selection

PUSH1 through PUSH8 are not mnemonics you write. In assembly, and through InstructionBuilder::push(), there is only PUSH <value>:

PUSH 42       ; assembler selects PUSH1 (fits in i8)
PUSH 1000     ; assembler selects PUSH2 (fits in i16)
PUSH -1       ; assembler selects PUSH1 (0xFF sign-extends to -1)

The assembler picks the narrowest PUSH1..PUSH8 variant that faithfully round-trips the literal, so small constants cost 2 bytes total (1 opcode + 1 operand byte) and the full 8-byte PUSH8 is only ever emitted for values that need all 64 bits. In the program above, PUSH 42 assembles to PUSH1 42 (0x2A), PUSH 1000 assembles to PUSH2 1000 (0x03E8), and PUSH -1 assembles to PUSH1 -1, whose single operand byte is 0xFF.

Writing PUSH1 directly does not work: it is not a recognised mnemonic, and the assembler rejects it with unknown mnemonic 'PUSH1'. PUSHC is accepted as an alias for PUSH and goes through the same size-selection path; there is no reason to prefer one over the other beyond house style.

The encoding itself is big-endian and sign-extended regardless of which PUSHn variant is chosen, which is what makes PUSH -1 a 1-byte operand rather than an 8-byte one: PUSH1 0xFF decodes as \(-1_{i64}\), not \(255_{i64}\), because the single byte is read as signed and then sign extended, not as an unsigned magnitude.

Arithmetic

The usual signed-integer operations, all on i64, plus two helpers that show up often enough in penalty-weight and slack-variable arithmetic to earn their own opcodes: SQR, a one-instruction shorthand for COPY plus MUL, and BITLEN, which the VM has no other way to compute at all, since there is no logarithm instruction to compose it from. Byte values, operand layouts and stack effects are in the Arithmetic section of the opcode reference. None of these instructions touch a register; every operand comes off the stack and every result goes back onto it.

Overflow

Every arithmetic instruction on this page is checked. ADD, SUB, MUL, NEG, ABS, SQR, INC and DEC raise ArithmeticOverflow when the result would leave the i64 range, rather than wrapping to a value the program never asked for:

PUSH 9223372036854775807   ; i64::MAX
PUSH 1
ADD                        ; raises ArithmeticOverflow

The rule is on the result, not on the machine operation underneath it. ABS and NEG of i64::MIN raise, because no positive i64 has that magnitude. i64::MIN / -1 raises for the same reason. i64::MIN % -1 does not: the remainder is 0, which is perfectly representable, even though a fixed-width machine reaches it through an overflowing division.

Values built from several operations – a constraint expansion’s coefficients, an ENERGY accumulation – are checked at every step, so a computation that leaves the range on its way to an in-range answer raises rather than quietly recovering. Both reference interpreters implement the same rule, so a program that raises on one raises on the other.

Division and Remainder

DIV rounds toward negative infinity: it is floor division, matching Python’s //, not Rust’s default /, which truncates toward zero. MOD returns a remainder with the sign of the divisor, matching Python’s %, so the identity \(a = \lfloor a / b \rfloor \cdot b + (a \bmod b)\) holds under floored division. PUSH -7; PUSH 2; DIV pushes -4, not the -3 that truncating division would give, and PUSH -7; PUSH 2; MOD pushes 1, not -1. Both error with DivisionByZero when the divisor is zero rather than wrapping or producing a sentinel value: PUSH 5; PUSH 0; DIV halts execution with division by zero instead of continuing.

MIN, MAX and BITLEN

MIN and MAX are signed comparisons with no domain restriction. BITLEN pops a value and pushes the number of bits needed to represent it in binary, \(\lfloor\log_2(a)\rfloor + 1\), returning \(0\) for non-positive input rather than erroring (there is no bit length for a number that is not positive, and 0 is a more useful sentinel here than a fault, since BITLEN output typically feeds straight into SLACK’s capacity calculation). BITLEN(1) = 1, BITLEN(7) = 3, BITLEN(8) = 4, BITLEN(255) = 8.

Comparison

EQ, LT, GT, LTE and GTE compare two signed i64 values and push exactly 1 (true) or 0 (false). Byte values, operand layouts and stack effects are in the Comparison section of the opcode reference. None of these instructions touch a register, and none of them can fail beyond the ordinary StackUnderflow every popping instruction shares; every i64 value is comparable to every other.

The Iverson bracket notation \([P]\), used throughout this book, equals \(1\) if \(P\) is true and \(0\) otherwise; EQ a b is exactly \([a = b]\), and the other four follow the same pattern.

Boolean Convention

XQVM uses the integer convention for booleans: 0 is false, any non-zero value is true. Comparison instructions always produce exactly 1 or 0, never some other non-zero encoding of “true”, which is what makes their output directly usable as a JUMPI condition or as an operand to Logical AND/OR/XOR/NOT without an extra normalisation step. PUSH 3; PUSH 5; LT pushes 1, and PUSH 3; PUSH 5; GT pushes 0, both read back through STOW/OUTPUT.

Logical Boolean

NOT, AND, OR and XOR treat their operands as booleans under the integer convention: 0 is false, any non-zero value, including negative values, is true. Results are always exactly 1 or 0. Byte values, operand layouts and stack effects are in the Logical Boolean section of the opcode reference. None of these instructions touch a register.

AND, OR and XOR each pop both operands unconditionally before producing a result; there is no short-circuit evaluation the way there would be in a language with lazy boolean operators, since a stack VM has already evaluated both operand expressions and pushed both values by the time the logical instruction runs. This only matters if evaluating one of the operand expressions has a side effect, register writes, or a Coefficient Access mutation, that a short-circuiting language would skip; AND/OR/XOR never skip it.

Logical vs. Bitwise

These instructions perform boolean operations on the truthiness of a value, not on its bit pattern. For bit-pattern operations, see Bitwise (BAND, BOR, BXOR, BNOT). The two families diverge sharply outside {0, 1} inputs: NOT 5 is 0 (5 is truthy, so its logical negation is false), while BNOT 5 is ~5, the bitwise complement, a large negative number (-6). NOT on 5 returns 0, and BNOT on 5 returns -6, from the same program run against both instructions.

Bitwise

BAND, BOR, BXOR and BNOT operate on the raw two’s-complement bit pattern of an i64; SHL and SHR shift it. Byte values, operand layouts and stack effects are in the Bitwise section of the opcode reference. None of these instructions touch a register, and none but the shifts can fail: BAND/BOR/BXOR/BNOT are defined for every i64 bit pattern, with no invalid input. SHL and SHR fault on a shift amount outside \([0, 64)\), and SHL also faults when the shift would lose a significant bit.

For the boolean-algebra equivalents that treat 0/non-zero as false/true instead of operating bit by bit, see Logical.

Shift Behaviour

SHL performs a left shift, filling the vacated low bits with zero. It does not discard bits that leave the high end: a shift that loses a significant bit takes the value outside the i64 range, so it raises ArithmeticOverflow like any other overflowing operation. PUSH 4611686018427387904; PUSH 1; SHL fails rather than yielding i64::MIN. Shifting the result back recovers the operand exactly whenever nothing was lost, which is the test the VM applies.

SHR performs an arithmetic (sign-preserving) right shift, not a logical (zero-filling) one: the sign bit is replicated into the vacated high bits, so a negative operand stays negative. -8 >> 1 yields -4, and i64::MIN >> 1 yields -4611686018427387904, half the magnitude of i64::MIN and still negative, rather than a small positive number a logical shift would produce. This matches Rust’s i64 >> b operator and Python’s >> on integers.

Both instructions require the shift amount to satisfy \(0 \le b < 64\) and error InvalidShift otherwise; a shift by 64 or more is not defined as “shift everything out to zero” the way it might be on some hardware, it is rejected outright: PUSH 8; PUSH 64; SHR fails with invalid shift amount 64.

Where a logical right shift is actually needed, mask the sign-extended bits off after SHR with BAND. For a shift of exactly 1, BAND with 0x7FFFFFFFFFFFFFFF (i64::MAX) clears only the replicated sign bit, which is everything an arithmetic shift by 1 could have filled from the sign: SHR -8 1 followed by masking with i64::MAX gives 9223372036854775804, the same result a zero-filling right shift by 1 would give. A shift by more than 1 replicates the sign into more than one bit, so the mask has to widen to match: BAND with a fixed i64::MAX mask only emulates a logical shift for b = 1.

Allocators

Every problem starts here: allocate a Model to accumulate coefficients into, or a Sample to hold a candidate assignment, or a Vec to stage indices and weights before a high-level constraint call. Byte values, operand layouts and stack effects for this family are in the Allocators and Vector Operations sections of the opcode reference; this page is about the three domains and when each applies.

Model Allocators

BQMX and SQMX each pop a variable count, size, and write a fresh XqmxModel into a register, with empty linear and quadratic coefficient maps. XQMX pops two values, k (top of stack) then size, since an integer model also needs the per-variable domain width. The model holds the Hamiltonian being built up:

$$H(x) = \sum_i \text{linear}[i] \cdot x_i + \sum_{i \le j} \text{quadratic}[i, j] \cdot x_i \cdot x_j$$

which Coefficient Access instructions populate one term at a time. What differs between the three allocators is the domain the variables \(x_i\) are drawn from, and that choice is a problem-modelling decision, not an implementation detail:

  • BQMX allocates a QUBO model: variables are binary, \(x_i \in \{0, 1\}\). This is the domain most combinatorial-optimisation formulations target directly (selection, assignment, one-hot encodings), and the one most solver backends consume without translation.
  • SQMX allocates an Ising model: variables are spins, \(x_i \in \{-1, 1\}\). Physically motivated (each variable is a magnetic moment pointing up or down), and the native representation for quantum annealers, which minimise an Ising Hamiltonian directly. Allocate the domain the target backend expects rather than assuming a QUBO model can be handed to an Ising-only backend unchanged.
  • XQMX allocates an integer model: variables take one of \(k\) values, \(x_i \in \{0, 1, \ldots, k{-}1\}\). \(k\) is a count and not a half-width. This generalises past binary and spin to give a variable more than two states directly, suited to a quantity with a natural ordering or magnitude – a position in a small enumerated set – without one-hot-encoding it into several binary variables first. It does not encode an unordered categorical choice such as a colour: a quadratic form over integer variables cannot express that two values merely differ without also expressing by how much. XQMX errors with InvalidIntegerK when \(k < 2\), since a domain needs at least two values to carry a decision: \(k = 1\) leaves the single value \(\{0\}\), which is a constant rather than a variable.

Sample Allocators

BSMX, SSMX and XSMX mirror the three model allocators, including the same pop count per opcode – BSMX and SSMX pop only size; XSMX pops k then size, the same order as XQMX – but write an XqmxSample: a vector of per-variable assignments in the same domain, rather than a coefficient map. A sample is what a solver returns, or what ENERGY evaluates against a model to score a candidate solution.

The default assignment differs by domain and is chosen so it is always in-domain without a special case: binary and integer samples default every variable to \(0\), and spin samples default every variable to \(-1\) (spin-down), since \(0\) is not a member of \(\{-1, 1\}\). The integer default of \(0\) is always valid because the domain \(\{0, \ldots, k{-}1\}\) starts at zero for every \(k \ge 2\).

Those defaults are not merely conventional. SETLINE and ADDLINE check every write into a sample against its domain and raise SampleOutOfDomain otherwise, so a freshly allocated sample has to be in-domain from the start or the first read of an untouched variable would return a value the same register could not have been written.

Vec Allocators

VECI creates an empty VecInt and VECX creates an empty VecXqmx (a vector of models, used to batch several sub-models together). VEC is an untyped convenience form: at the bytecode level it produces exactly the same VecInt as VECI, so VEC r0 and VECI r0 are interchangeable. Prefer VECI in generated or reviewed bytecode, where being explicit about the element type documents intent; VEC reads naturally in hand-written assembly where the type is obvious from what gets pushed next.

Domain Types

DomainVariable valuesModelSample
Binary\(\{0, 1\}\)BQMXBSMX
Spin\(\{-1, 1\}\)SQMXSSMX
Integer(\(k\))\(\{0, 1, \ldots, k{-}1\}\), \(k \ge 2\)XQMXXSMX

Example

PUSH 4
PUSH 1
XQMX r0     ; errors: InvalidIntegerK, k = 1 is not >= 2

XQMX and XSMX both check k only after popping both operands, so the stack is consumed either way; on k < 2 they fault InvalidIntegerK with the message XQMX/XSMX requires k >= 2 for the {0, ..., k-1} domain, got k = 1.

Vector Operations

Instructions for reading, writing and querying the VecInt and VecXqmx containers that Allocators create. Byte values, operand layouts and stack effects are in the Vector Operations section of the opcode reference. This page covers the type rules that apply across the family and the details of SLACK, the one instruction here that does more than plain container access.

Reading, Writing and Sizing

VECPUSH appends to the end of a VecInt, growing it by one element. VECGET and VECSET read and write by index, both bounds-checked against the current length: an out-of-range index errors IndexOutOfBounds at runtime rather than reading past the end of the vector. VECLEN reads the current length as an i64, and is the only instruction in this family that accepts either VecInt or VecXqmx; the other three require VecInt specifically, since VecXqmx elements are whole models rather than integers and there is no VECXGET/VECXSET.

VEC r0          ; r0 = empty VecInt
PUSH 10
VECPUSH r0      ; r0 = [10]
PUSH 20
VECPUSH r0      ; r0 = [10, 20]
PUSH 0
VECGET r0       ; stack top = 10

An OUTPUT of the loaded value writes 10 to the output slot, and a VECLEN r0 issued after the two VECPUSHes reads 2.

SLACK

SLACK indices coeffs is the one instruction in this family that is not a plain accessor: it exists to turn an inequality constraint into an equality one, by appending binary-weighted slack variables to two parallel vecs. Pop capacity (top of stack), then start_index. Compute

$$S = \lfloor \log_2(\text{capacity}) \rfloor + 1$$

and append \(S\) entries to each register:

  • to indices: \([\text{start}, \text{start}{+}1, \ldots, \text{start}{+}S{-}1]\), consecutive variable indices for the new slack variables;
  • to coeffs: \([1, 2, 4, \ldots, 2^{S-1}]\), their binary weights.

SLACK appends rather than overwrites, so item variables and slack variables coexist in the same indices/coeffs pair, ready to hand to EQUALITY. This is what makes a knapsack-style “total weight at most capacity” constraint expressible as a single weighted equality: the slack variables absorb any unused capacity, so the equality holds exactly whenever the inequality would have held. If capacity <= 0, no slack variables are needed and SLACK appends nothing.

VEC r5            ; indices
VEC r6            ; coeffs
; ... populate with item indices and weights ...
PUSH 3            ; start_index (first slack variable index)
PUSH 10           ; capacity
SLACK r5 r6       ; appends 4 slack entries: floor(log2(10)) + 1 = 4

With start_index = 3, capacity = 10: VECLEN r5 after SLACK reads 4, indices starts at 3 and ends at 6 ([3, 4, 5, 6]), and coeffs is [1, 2, 4, 8], matching \(S = 4\) and the powers of two up to \(2^{S-1}\).

Index Math

IDXGRID and IDXTRIU compute the two flat-indexing schemes the rest of the VM relies on, so that bytecode which builds indices by hand does not have to reimplement either formula. Byte values, operand layouts and stack effects are in the Index Math section of the opcode reference. Neither instruction touches a register, and both use the same wrapping i64 arithmetic as Arithmetic; a sufficiently large IDXGRID product wraps rather than trapping, the same as MUL would.

IDXGRID: Row-Major Flat Index

IDXGRID pops C (columns), then c (column), then r (row), and pushes

$$\text{index} = r \cdot C + c$$

the same row-major convention RESIZE attaches to a model. Computing this by hand and computing it with IDXGRID produce identical results, since RESIZE does not change how flat indices are interpreted internally; IDXGRID exists so bytecode that needs the flat index as an ordinary stack value, for example to pass to SETLINE/SETQUAD without going through a grid instruction, does not have to spell out r * C + c as separate MUL and ADD instructions.

A worked example: a TSP over 4 cities and 4 positions models \(x[\text{city}][\text{position}]\) as a \(4 \times 4\) grid, so city 2 at position 1 is variable

PUSH 2        ; row = city = 2
PUSH 1        ; col = position = 1
PUSH 4        ; cols = 4
IDXGRID       ; index = 2 * 4 + 1 = 9

This program’s result, read back with STOW/OUTPUT, is 9.

IDXTRIU: Upper-Triangular Packed Index

IDXTRIU pops j, then i, and pushes

$$\text{index} = \frac{j \cdot (j - 1)}{2} + i \qquad (i < j)$$

the packed index for the pair \((i, j)\) in the strictly upper triangle of a symmetric matrix, useful for iterating over quadratic coefficient pairs without visiting \((i, j)\) and \((j, i)\) as two different positions. If i > j, IDXTRIU swaps them before computing the index, so the result is order-independent: \((i, j)\) and \((j, i)\) pack to the same value, the same guarantee SETQUAD/GETQUAD give by normalising the pair internally.

The enumeration is strictly upper-triangular, so it has no slot for a diagonal cell even though the quadratic table has one. SETQUAD and GETQUAD accept \(i = j\) and store a self-coupling; IDXTRIU called with \(i = j\) yields the index of some off-diagonal pair rather than of the diagonal one. Do not use it to address a diagonal coefficient.

IDXTRIU and IDXGRID do not range-check their operands, but every intermediate of the computation is checked, so a product or sum that leaves the i64 range raises ArithmeticOverflow even when the final index would land back inside it.

PUSH 1        ; i = 1
PUSH 3        ; j = 3
IDXTRIU       ; index = 3 * 2 / 2 + 1 = 4

This program’s result is 4.

Coefficient Access

Read and write the linear (bias) and quadratic (coupling) terms of the Hamiltonian

$$H(x) = \sum_i \text{linear}[i] \cdot x_i + \sum_{i \le j} \text{quadratic}[i, j] \cdot x_i \cdot x_j$$

that a Model accumulates. Byte values, operand layouts and stack effects are in the XQMX Coefficient Access section of the opcode reference. All coefficient values are i64.

Linear Access Reads and Writes Samples Too

GETLINE, SETLINE and ADDLINE accept reg holding either a Model or a Sample: on a Model register they read or write the sparse bias map, and on a Sample register the identical instruction reads or writes the sample’s per-variable assignment at the same index instead. GETLINE r0 on a freshly allocated BSMX sample (no coefficients, only assignments) returns the assigned value at that index.

xquad verify accepts the same programs. It did not until QUI-1168: xqvm/src/verifier/reg_type.rs grouped the linear trio with the quadratic and constraint opcodes under a single check requiring Model, so GETLINE r0 on the program above failed with register r0 at byte 0x0006: expected model, got sample even though the VM ran it. spec/xqvm/ISA.md had said all along that the linear opcodes accept either mode, so that was a defect in the verifier rather than a documented restriction, and it is fixed.

The quadratic trio, GETQUAD, SETQUAD and ADDQUAD, has no sample equivalent at either level and rejects a Sample register with RegisterType (“expected model, got sample”), since a sample has no coupling terms to read or write.

Sample Writes Are Domain-Checked

A sample’s entry is an assignment, so it has to be a value the variable can actually take. SETLINE and ADDLINE raise SampleOutOfDomain when the value they would store is not a member of the register’s domain – {0, 1} for BSMX, {-1, +1} for SSMX, {0, ..., k-1} for XSMX. Note that 0 is out of domain for spin: the spin domain has two members and a gap between them, so a check written as a range would wrongly admit it.

ADDLINE checks the result of the addition, not the delta. Adding 1 to a binary variable already holding 1 raises; adding -1 to it succeeds and leaves 0, even though -1 is not itself a binary value.

A Model register is never checked. Its linear[i] is a bias coefficient, whose magnitude is the weight the objective gives that variable and has nothing to do with the values the variable may take – a binary model routinely carries large negative biases.

The guarantee is about the two opcodes and not about the register. A host that installs a whole sample through calldata bypasses them, and GETLINE will read back whatever it installed; the Python and FFI bindings validate at that boundary instead.

Every Index Is Bounds-Checked

All six instructions bounds-check their popped indices against the register’s declared size (model.size or sample.values.len()) and error IndexOutOfBounds if an index is negative or at least size: GETLINE r0 for i = 99 on a 4-variable model fails with index 99 out of bounds (len 4), even though the coefficient map holds no fixed-size backing array. The size bound comes from the model’s declared variable count, not from the map itself, which is why an absent in-range coefficient reads as 0 while a read past the end raises.

Reads are bounded exactly as writes are. GETQUAD r0 for (i, j) = (99, 100) on a 4-variable model raises rather than answering 0, because a GETQUAD that reads back 0 from a pair SETQUAD refuses would leave the family disagreeing with itself about which variables exist.

SETQUAD and ADDQUAD check i before j, and both before the i > j normalisation below, so the reported index is the operand the program supplied rather than whichever one sorted lower.

The same bound covers EXCLUDE and IMPLIES, which write coefficients through the same surfaces. Nothing in either family can put a coefficient outside the variable count a BQMX/SQMX/XQMX call declared: an unbounded write would grow the sparse map and leave the model carrying a constraint over variables that do not exist, which still solves cleanly and answers wrongly.

Sparse Storage

Coefficients live in sparse BTreeMap structures, not a fixed-size array:

  • Linear: BTreeMap<usize, i64> keyed by variable index.
  • Quadratic: BTreeMap<(usize, usize), i64> keyed by variable pair.

An absent key reads as 0. Setting a coefficient to exactly 0 removes its entry rather than storing a zero, for both the linear and the quadratic map, so memory usage tracks the number of non-zero terms rather than the number of SETLINE/SETQUAD calls ever made; writing 0 and never reading that index again leaves no trace in the map.

Key Normalisation

Quadratic keys are normalised so that \(i \le j\): SETQUAD, ADDQUAD and GETQUAD all swap i and j internally when i > j, so \(\text{quad}[3, 5]\) and \(\text{quad}[5, 3]\) name the same entry. SETQUAD r0 with (i, j) = (5, 3) followed by GETQUAD r0 with (i, j) = (3, 5) returns the value just written, not 0.

Grid Operations

A model’s linear surface is stored flat, indexed by a single usize. Grid operations let it additionally be addressed as a 2-D matrix, \((\text{row}, \text{col})\), by attaching row and column counts as metadata and reinterpreting the flat index as \(\text{row} \cdot \text{cols} + \text{col}\), the same convention IDXGRID computes by hand. Byte values, operand layouts and stack effects are in the XQMX Grid section of the opcode reference.

Every instruction here accepts reg holding either a Model or a Sample, not only a Model: ROWFIND, COLFIND, ROWSUM and COLSUM read a “linear surface” that is the model’s sparse coefficient map for a Model register, and the sample’s dense assignment vector for a Sample register, so the same instruction reads either a model’s biases or a solved sample’s variable values depending on what is in the register. ROWSUM on a BSMX sample (no coefficients to speak of, only assignments) raises no RegisterType error and returns the row’s summed values. A sample has to be gridded first, though: a freshly allocated one carries rows = cols = 0, and all four opcodes raise InvalidGridDimensions on a register with no grid.

RESIZE Attaches, It Does Not Reshape

RESIZE pops cols then rows and stores them on the model or sample; it does not touch the coefficient map or assignment vector at all. A model with 16 variables and no grid set, and the same model after RESIZE r0 with rows = 4, cols = 4, have byte-for-byte identical linear and quadratic maps; what changes is how ROWFIND, COLFIND, ROWSUM, COLSUM and, outside this page, ONEHOTR/ONEHOTC interpret a flat index. A grid has to satisfy two conditions. Both rows and cols must be strictly positive, so RESIZE r0 with either argument \(\le 0\) errors at runtime with invalid grid dimensions, for example with rows = 0, cols = 4. And \(\text{rows} \cdot \text{cols}\) must not exceed the register’s declared size: a grid is a reinterpretation of variables the program already declared, so it cannot describe cells that do not exist. RESIZE r0 to \(3 \times 3\) on a 4-variable model raises InvalidGridDimensions for the same reason a negative row count does.

The extent bound is \(\le\), not \(=\). EQUALITY, ATLEAST, ATLEASTW and REDUCE append slack and auxiliary variables past the grid and nothing ever shrinks a register’s size, so a model whose size exceeds its extent is the normal state after any of them; a later RESIZE over a strict subset of the variables is still accepted.

Both checks are runtime checks, not ones the verifier catches statically, since grid dimensions are ordinary popped stack values rather than something the verifier’s dataflow passes track.

The canonical use is encoding a two-index variable directly instead of computing flat indices by hand at every access site. A 4-city TSP, for example, models \(x[\text{city}][\text{position}]\) as a \(4 \times 4\) grid:

PUSH 16        ; size = 4 * 4
BQMX r0        ; allocate binary model
PUSH 4         ; rows = 4
PUSH 4         ; cols = 4
RESIZE r0      ; set grid dimensions

after which row 2 is every variable for city 2 across all four positions, and ONEHOTR r0 over row 2 is exactly the constraint “city 2 occupies exactly one position”.

Row and Column Bounds Are Enforced

ROWFIND, COLFIND, ROWSUM and COLSUM each check the index they pop against the grid axis they address. A negative index and an index at or past the declared extent both raise IndexOutOfBounds, naming the index and the extent it exceeded. ROWSUM r0 for row = 99 on a model RESIZEd to \(2 \times 2\) raises rather than returning 0:

PUSH 4
BQMX r0
PUSH 2         ; rows
PUSH 2         ; cols
RESIZE r0
PUSH 99        ; row -- past the two rows the grid declares
ROWSUM r0
HALT
Error: xqvm::runtime_error

  × index 99 out of bounds (len 2) at byte 0x000c

A register with no grid at all is a separate fault: rows = cols = 0 addresses no line, so all four opcodes raise InvalidGridDimensions rather than reducing over nothing. That is the same identity ONEHOTR and ONEHOTC raise without a grid.

RESIZE’s dimensions are a bound the VM enforces, not a convention correct bytecode is trusted to honour. Both implementations agree on all three cases, and conformance/vectors/xqmx-grid/ pins them.

ROWFIND and COLFIND

ROWFIND pops v then r, scans row r left to right, and pushes the column of the first entry whose value equals v, or \(-1\) if none matches. COLFIND pops v then c and is the column-major mirror. Since a model’s storage is sparse, an unset coefficient reads as 0 (see Coefficient Access), so searching for v = 0 against a model can match either an explicit zero or nothing at all, depending on write history. An unmatched search returns \(-1\) rather than erroring, but only once the search has run: the register still has to carry a grid, and r still has to name a row that grid declares. \(-1\) means “scanned, no match”, never “no such row”. On a \(2 \times 2\) model with linear[0] = 9 and nothing else set, ROWFIND for v = 9 in row 0 returns 0 (the match), and the identical search in row 1 returns \(-1\); the same search in row 2 raises IndexOutOfBounds.

ROWSUM and COLSUM

ROWSUM pops r and pushes \(s = \sum_{c=0}^{C-1} \text{linear}[r \cdot C + c]\); COLSUM pops c and pushes the column-major equivalent. Both sum over the full declared row or column width, treating absent model entries as 0 rather than skipping them. On a Sample register the same sum runs over the sample’s assignment values instead of coefficients, which is how bytecode checks a solved one-hot row without decoding each variable index by hand: after a solver’s result is loaded back into a Sample register, ROWSUM over a row that carried a ONEHOTR constraint should read exactly 1 if the constraint holds in that sample.

High-Level Constraints

These instructions inject QUBO penalty terms for common combinatorial constraints, expanding into linear and quadratic coefficient deltas automatically. The model register must hold a Model in model mode. Grid-based opcodes (ONEHOTR, ONEHOTC) read the grid dimensions set by RESIZE and require them, in both halves of the grid precondition spec/xqvm/ISA.md states normatively. On a model with no grid set (rows and cols both 0) both raise InvalidGridDimensions rather than expanding over nothing, so forgetting RESIZE fails at the instruction that needed it rather than producing a model missing a constraint. A row outside [0, rows), or a col outside [0, cols), raises IndexOutOfBounds rather than applying the constraint to variables the grid does not address – the same fault the four grid opcodes raise for an out-of-range row or column. Both implementations agree on both halves. xquad verify cannot catch either, because the grid dimensions and the index alike are popped stack values rather than something the verifier’s dataflow passes track. Vec-based opcodes (EQUALITY, ATLEAST, ATLEASTW, REDUCE) operate on arbitrary variable sets. All coefficients are i64. For each opcode’s byte value and operand layout, see the Opcode Reference.

ONEHOTR reg

Register effect: mutate

Pop penalty, then row. Apply the one-hot constraint over all variables in grid row row:

$$H \mathrel{+}= \text{penalty} \cdot \left(\sum_c x_{\text{row},c} - 1\right)^2$$

Expanding (binary variables: \(x^2 = x\)):

$$\text{linear}[\text{row} \cdot \text{cols} + c] \mathrel{+}= -\text{penalty} \qquad \forall; c \in [0, \text{cols})$$

$$\text{quad}[\text{row} \cdot \text{cols} + c_i,; \text{row} \cdot \text{cols} + c_j] \mathrel{+}= 2 \cdot \text{penalty} \qquad \forall; c_i < c_j$$

ONEHOTC reg

Register effect: mutate

Pop penalty, then col. One-hot over all variables in grid column col:

$$\text{linear}[r_i \cdot \text{cols} + \text{col}] \mathrel{+}= -\text{penalty} \qquad \forall; r_i \in [0, \text{rows})$$

$$\text{quad}[r_i \cdot \text{cols} + \text{col},; r_j \cdot \text{cols} + \text{col}] \mathrel{+}= 2 \cdot \text{penalty} \qquad \forall; r_i < r_j$$

EXCLUDE reg

Register effect: mutate

Pop penalty, then \(j\), then \(i\). Add mutual-exclusion: penalise \(x_i = 1\) and \(x_j = 1\) simultaneously.

$$\text{quad}[i, j] \mathrel{+}= \text{penalty}$$

IMPLIES reg

Register effect: mutate

Pop penalty, then \(j\), then \(i\). Add implication \(i \Rightarrow j\): penalise \(x_i = 1\) with \(x_j = 0\).

$$H \mathrel{+}= \text{penalty} \cdot x_i \cdot (1 - x_j) = \text{penalty} \cdot x_i - \text{penalty} \cdot x_i \cdot x_j$$

$$\text{linear}[i] \mathrel{+}= \text{penalty}$$

$$\text{quad}[i, j] \mathrel{+}= -\text{penalty}$$

EQUALITY model indices coeffs

Register effect: read indices, coeffs; mutate model

Pop penalty, then target. Read variable indices from indices (VecInt) and coefficients from coeffs (VecInt). Expand the weighted equality constraint into QUBO terms on model:

$$H \mathrel{+}= P \cdot \left(\sum_k a_k \cdot x_{\text{idx}_k} - b\right)^2$$

Expanding:

$$\text{linear}[\text{idx}_k] \mathrel{+}= P \cdot a_k \cdot (a_k - 2b) \qquad \forall; k$$

$$\text{quad}[\text{idx}_k, \text{idx}_m] \mathrel{+}= 2P \cdot a_k \cdot a_m \qquad \forall; k < m$$

The constant term \(P \cdot b^2\) is dropped. EQUALITY is the general form of ONEHOTR/ONEHOTC – setting all \(a_k = 1\) and \(b = 1\) produces the same expansion.

If an index in indices is at or past the model’s current size, EQUALITY grows model.size to fit it rather than erroring – unlike ATLEAST and ATLEASTW below, which validate incoming indices against the model’s existing size and raise IndexOutOfBounds on an out-of-range one. Otherwise, indices and coeffs must have equal length or the instruction raises VecLengthMismatch.

ATLEAST model indices

Register effect: read indices; mutate model (grows size)

Pop penalty, then \(k\). Read variable indices from indices. Enforce \(\sum x_i \ge k\) by allocating \(S = \lfloor\log_2(N - k)\rfloor + 1\) slack variables at model.size and applying an EQUALITY expansion with target \(k\), where \(N\) is the number of indices:

$$\sum_i x_{\text{idx}i} - \sum{j=0}^{S-1} 2^j \cdot s_j = k$$

This formula covers \(N - k > 0\). When \(N - k \le 0\) – only possible at \(k = N\), since IndexOutOfBounds below already rejects \(k > N\) – the constraint is already an equality with nothing left to slacken, so ATLEAST allocates zero slack variables and applies the EQUALITY expansion directly, with no \(S\) term at all.

Raises IndexOutOfBounds if \(k \le 0\) or \(k > N\), and if any index in indices is at or past the model’s existing size – ATLEAST does not grow the model to fit an out-of-range input index, only to hold the slack variables it allocates itself.

ATLEASTW model indices coeffs

Register effect: read indices, coeffs; mutate model (grows size)

Pop penalty, then \(k\). Same as ATLEAST but with arbitrary weights from coeffs. Enforces \(\sum w_i \cdot x_i \ge k\). The slack count is computed from \(\text{max_excess} = \sum w_i - k\).

Raises VecLengthMismatch if indices and coeffs have different lengths, or IndexOutOfBounds if \(k \le 0\).

REDUCE model

Register effect: mutate model (grows size)

Pop \(P_{\text{aux}}\), then \(\text{var_b}\), then \(\text{var_a}\). Allocate auxiliary variable \(w\) at model.size. Add Rosenberg enforcement terms constraining \(w = x_a \cdot x_b\):

$$\text{quad}[\text{var_a}, \text{var_b}] \mathrel{+}= P_{\text{aux}}$$

$$\text{quad}[\text{var_a}, w] \mathrel{+}= -2 P_{\text{aux}}$$

$$\text{quad}[\text{var_b}, w] \mathrel{+}= -2 P_{\text{aux}}$$

$$\text{linear}[w] \mathrel{+}= 3 P_{\text{aux}}$$

Push \(w\) (the auxiliary index). Enables chaining for higher-order terms: reduce a quartic \(x_i x_j x_k x_l\) by calling REDUCE twice to get \(w_1 = x_i x_j\) then \(w_2 = w_1 x_k\), and finish with ADDQUAD on \((w_2, x_l)\).

Usage Pattern

Constraint instructions are designed to work with grid models. A typical pattern for a TSP-style assignment grid:

; Allocate model and set grid
PUSH 16
BQMX r0
PUSH 4
PUSH 4
RESIZE r0

; Apply one-hot constraints on each row and column
PUSH 0
PUSH 4
RANGE
  LVAL r1
  LOAD r1
  PUSH 100       ; penalty weight
  ONEHOTR r0     ; each city visits exactly one position
NEXT

PUSH 0
PUSH 4
RANGE
  LVAL r1
  LOAD r1
  PUSH 100
  ONEHOTC r0     ; each position has exactly one city
NEXT

HALT

Energy Evaluation

ENERGY is the sole instruction in this category: it evaluates a model’s Hamiltonian against a candidate sample and pushes the result. Byte value, operand layout and stack effect are in the XQMX High-Level Constraints section of the opcode reference. See Allocators for how a Model and a Sample are built in the first place.

ENERGY model sample

Register effect: read – both model and sample are read-only

ENERGY takes two register operands, model and sample. Both checks are strict: the model register must hold a Model, the sample register must hold a Sample, and a RegisterType error is raised if either register holds any other variant – a Model cannot be passed in the sample slot or vice versa.

To populate a sample with concrete variable assignments, construct an xqvm::XqmxSample in the host and pass it to the program through a calldata slot, then INPUT it into a register before calling ENERGY. SETLINE and ADDLINE can also write a sample’s per-variable assignment values in place, the same way they write a model’s linear bias map, and xquad verify accepts them – see Coefficient Access. Each write is checked against the sample’s domain, so a program can only build assignments the variables can actually take. Constructing an XqmxSample in the host and passing it through calldata remains the way to hand a verified program a whole candidate solution at once.

Hamiltonian

Evaluates the quadratic Hamiltonian:

$$E = \sum_{i} \text{linear}[i] \cdot x_i ;+; \sum_{i \le j} \text{quad}[i,j] \cdot x_i \cdot x_j$$

where \(x_i\) is sample.values[i], the variable assignment at index \(i\). The sum runs over \(i \le j\): the diagonal is legal, and a self-coupling quad[i,i] is evaluated as \(\text{quad}[i,i] \cdot x_i \cdot x_i\) like any other entry. The result is pushed as i64.

Overflow Is a Fault, Not a Wrap

Every term product and every partial sum is range-checked, and one that leaves the i64 range raises ArithmeticOverflow. The check is per step rather than on the final total, so a model whose mathematical energy is perfectly representable still faults when an intermediate is not. Accumulation runs in sorted key order, linear terms before quadratic ones, and that order is normative precisely because it decides which partial sums a checked implementation sees.

Take a 3-variable binary model with linear = {0: i64::MAX, 1: 1, 2: -1} and a sample assigning all three variables 1. The energy is \(2^{63} - 1 + 1 - 1 = 2^{63} - 1\), which is i64::MAX and representable. Wrapping arithmetic would have computed it correctly, overflowing to i64::MIN at the second term and back at the third. Per-step checking raises at the second term instead:

Error: xqvm::runtime_error

  × arithmetic overflow

Reordering the model’s coefficients so no partial sum leaves the range makes the same program succeed. A program that relies on cancellation between large terms has to be written to keep every running total representable.

Errors

  • RegisterType – if model is not a Model or sample is not a Sample.
  • SizeMismatch – if \(\lvert\text{sample}\rvert \neq \text{model.size}\).
  • ArithmeticOverflow – if any term product or partial sum leaves the i64 range.

Example

; Build a 2-variable binary model in r0:
;   linear[0] = 3, linear[1] = -2, quad[0,1] = 5.
PUSH 2
BQMX r0

PUSH 0
PUSH 3
SETLINE r0
PUSH 1
PUSH -2
SETLINE r0

PUSH 0
PUSH 1
PUSH 5
SETQUAD r0

; A freshly-allocated binary sample is initialised to all zeros, so
; H(0, 0) = 0.
PUSH 2
BSMX r1

ENERGY r0 r1
HALT

In this example, the sample is [0, 0] and the Hamiltonian evaluates to \(E = 0\). To exercise a non-zero assignment, construct an XqmxSample in host code with XqmxSample::new(Domain::Binary, vec![1, 1]) and INPUT it into r1 before calling ENERGY.

Opcode Reference

Concise reference table for every opcode in the XQVM bytecode format. Derived directly from conformance/opcodes.yaml, which is kept in sync with the Rust opcodes! x-macro and the Python Opcode enum.

For the normative bytecode specification, see spec/xqvm/SPEC.md.

Columns:

  • Code – wire-encoding byte.
  • Mnemonic – uppercase assembly name.
  • Operands – post-opcode operand layout; empty for no-operand instructions.
  • Stack – stack effect as pop → push; 0 → 1 means one value produced. any → 0 marks an instruction that empties the stack outright rather than applying a fixed net effect.
  • Description – single-sentence semantic summary.

Reserved wire bytes (rejected by the decoder as illegal): 0x0D, 0x19, 0x1D-0x1F, 0x2D-0x2F, 0x35, 0x46-0x49, 0x4D-0x4F, 0x55-0x59, 0x5C-0x5F, 0x6B-0x6F, 0x78-0x7E, 0x80-0xEF, 0xF1-0xFE.

Total: 93 opcodes.


Control Flow

CodeMnemonicOperandsStackDescription
0x00TARGET0 → 0Mark a valid jump destination.
0x01JUMP1label: u80 → 0Unconditionally jump to a basic block by u8 label index (narrow form).
0x02JUMPI1label: u81 → 0Jump to a basic block by u8 label index if the top of the stack is non-zero (narrow form).
0x03JUMP2label: u160 → 0Unconditionally jump to a basic block by u16 label index (wide form).
0x04JUMPI2label: u161 → 0Jump to a basic block by u16 label index if the top of the stack is non-zero (wide form).
0x05LIDXreg: Register0 → 0Copy the current loop index (offset-adjusted) into a register.
0x06LVALreg: Register0 → 0Copy the current loop value into a register.
0x07NEXT0 → 0Advance the loop index; jump back or exit the current loop.
0x08RANGE2 → 0Start a range loop over [start, start + count).
0x09ITERreg: Register2 → 0Start a vec iteration over a slice of a register’s vec.

Register I/O

CodeMnemonicOperandsStackDescription
0x0ALOADreg: Register0 → 1Push the value of an int register onto the stack.
0x0BSTOWreg: Register1 → 0Pop the top of the stack into an int register.
0x0CDROPreg: Register0 → 0Reset a register to Unset, releasing any value it held.
0x0EINPUTreg: Register1 → 0Pop a calldata slot index and load that slot into a register.
0x0FOUTPUTreg: Register1 → 0Pop an output slot index and write the register to it.

Stack Manipulation

CodeMnemonicOperandsStackDescription
0x10POP1 → 0Discard the top of the stack.
0x11PUSH1val: [u8; 1]0 → 1Push a 1-byte big-endian signed constant, sign-extended to i64.
0x12PUSH2val: [u8; 2]0 → 1Push a 2-byte big-endian signed constant, sign-extended to i64.
0x13PUSH3val: [u8; 3]0 → 1Push a 3-byte big-endian signed constant, sign-extended to i64.
0x14PUSH4val: [u8; 4]0 → 1Push a 4-byte big-endian signed constant, sign-extended to i64.
0x15PUSH5val: [u8; 5]0 → 1Push a 5-byte big-endian signed constant, sign-extended to i64.
0x16PUSH6val: [u8; 6]0 → 1Push a 6-byte big-endian signed constant, sign-extended to i64.
0x17PUSH7val: [u8; 7]0 → 1Push a 7-byte big-endian signed constant, sign-extended to i64.
0x18PUSH8val: [u8; 8]0 → 1Push a full 8-byte big-endian signed constant (i64).
0x1ASCLRany → 0Clear the entire value stack.
0x1BSWAP2 → 2Swap the top two stack elements.
0x1CCOPY1 → 2Duplicate the top of the stack.

Arithmetic

CodeMnemonicOperandsStackDescription
0x20ADD2 → 1Pop b and a; push a + b.
0x21SUB2 → 1Pop b and a; push a - b.
0x22MUL2 → 1Pop b and a; push a * b.
0x23DIV2 → 1Pop b and a; push a / b (floor division, rounds toward negative infinity).
0x24MOD2 → 1Pop b and a; push a % b.
0x25SQR1 → 1Pop a; push a * a.
0x26ABS1 → 1Pop a; push |a|.
0x27NEG1 → 1Pop a; push -a.
0x28MIN2 → 1Pop b and a; push min(a, b).
0x29MAX2 → 1Pop b and a; push max(a, b).
0x2AINC1 → 1Pop a; push a + 1.
0x2BDEC1 → 1Pop a; push a - 1.
0x2CBITLEN1 → 1Pop a; push floor(log2(a))+1. If a <= 0, push 0.

Comparison

CodeMnemonicOperandsStackDescription
0x30EQ2 → 1Pop b and a; push 1 if a == b, else 0.
0x31LT2 → 1Pop b and a; push 1 if a < b, else 0.
0x32GT2 → 1Pop b and a; push 1 if a > b, else 0.
0x33LTE2 → 1Pop b and a; push 1 if a <= b, else 0.
0x34GTE2 → 1Pop b and a; push 1 if a >= b, else 0.

Logical Boolean

CodeMnemonicOperandsStackDescription
0x36NOT1 → 1Pop a; push 1 if a == 0, else 0.
0x37AND2 → 1Pop b and a; push 1 if both are non-zero, else 0.
0x38OR2 → 1Pop b and a; push 1 if either is non-zero, else 0.
0x39XOR2 → 1Pop b and a; push 1 if exactly one is non-zero, else 0.

Bitwise

CodeMnemonicOperandsStackDescription
0x3ABAND2 → 1Pop b and a; push a & b.
0x3BBOR2 → 1Pop b and a; push a | b.
0x3CBXOR2 → 1Pop b and a; push a ^ b.
0x3DBNOT1 → 1Pop a; push ~a.
0x3ESHL2 → 1Pop b and a; push a << b.
0x3FSHR2 → 1Pop b and a; push a >> b (arithmetic right shift, sign-preserving).

Allocators

CodeMnemonicOperandsStackDescription
0x40BQMXreg: Register1 → 0Pop size; allocate a binary QUBO model ({0, 1} domain) into a register.
0x41SQMXreg: Register1 → 0Pop size; allocate a spin Ising model ({-1, +1} domain) into a register.
0x42XQMXreg: Register2 → 0Pop k then size; allocate an integer model with domain {0, …, k-1} into a register. Errors when k < 2.
0x43BSMXreg: Register1 → 0Pop size; allocate a binary sample ({0, 1} domain) into a register.
0x44SSMXreg: Register1 → 0Pop size; allocate a spin sample ({-1, +1} domain) into a register.
0x45XSMXreg: Register2 → 0Pop k then size; allocate an integer sample with domain {0, …, k-1} into a register. Errors when k < 2.
0x4AVECreg: Register0 → 0Create an empty vec<int> in a register, identical to VECI.
0x4BVECIreg: Register0 → 0Create an empty vec<int> in a register.
0x4CVECXreg: Register0 → 0Create an empty vec<xqmx> in a register.

Vector Operations

CodeMnemonicOperandsStackDescription
0x50VECPUSHreg: Register1 → 0Pop a value; append it to the register’s vec.
0x51VECGETreg: Register1 → 1Pop index; push vec[index] from the register’s vec.
0x52VECSETreg: Register2 → 0Pop value and index; set vec[index] in the register’s vec.
0x53VECLENreg: Register0 → 1Push the length of the register’s vec onto the stack.
0x54SLACKindices: Register, coeffs: Register2 → 0Pop capacity and start_index; append slack variable indices and power-of-two coefficients to two register vecs.

Index Math

CodeMnemonicOperandsStackDescription
0x5AIDXGRID3 → 1Pop cols, col, row; push the flat grid index row * cols + col.
0x5BIDXTRIU2 → 1Pop j and i; push the upper-triangular index for the unordered pair (i, j).

XQMX Coefficient Access

CodeMnemonicOperandsStackDescription
0x60GETLINEreg: Register1 → 1Pop i; push linear[i] from the register’s model, or a sample’s assignment at i; raises IndexOutOfBounds outside [0, size).
0x61SETLINEreg: Register2 → 0Pop value and i; set linear[i] in the register’s model, or a sample’s assignment at i; a sample value outside its domain raises SampleOutOfDomain.
0x62ADDLINEreg: Register2 → 0Pop delta and i; add delta to linear[i] in the register’s model, or a sample’s assignment at i; a sample result outside its domain raises SampleOutOfDomain.
0x63GETQUADreg: Register2 → 1Pop j and i; push quadratic[i, j] from the register’s model; requires MODEL mode – a sample register raises TypeMismatch; raises IndexOutOfBounds outside [0, size).
0x64SETQUADreg: Register3 → 0Pop value, j, and i; set quadratic[i, j] in the register’s model; requires MODEL mode – a sample register raises TypeMismatch; raises IndexOutOfBounds outside [0, size).
0x65ADDQUADreg: Register3 → 0Pop delta, j, and i; add delta to quadratic[i, j] in the register’s model; requires MODEL mode – a sample register raises TypeMismatch; raises IndexOutOfBounds outside [0, size).

XQMX Grid

CodeMnemonicOperandsStackDescription
0x66RESIZEreg: Register2 → 0Pop cols and rows; set the grid dimensions of the register’s model or sample.
0x67ROWFINDreg: Register2 → 1Pop value and row; push the first column where the value matches, or -1.
0x68COLFINDreg: Register2 → 1Pop value and col; push the first row where the value matches, or -1.
0x69ROWSUMreg: Register1 → 1Pop row; push the sum of all linear values in that grid row.
0x6ACOLSUMreg: Register1 → 1Pop col; push the sum of all linear values in that grid column.

XQMX High-Level Constraints

CodeMnemonicOperandsStackDescription
0x70ONEHOTRreg: Register2 → 0Pop penalty and row; add a one-hot constraint over the grid row.
0x71ONEHOTCreg: Register2 → 0Pop penalty and col; add a one-hot constraint over the grid column.
0x72EXCLUDEreg: Register3 → 0Pop penalty, j, and i; add a mutual-exclusion constraint between variables i and j; raises IndexOutOfBounds outside [0, size).
0x73IMPLIESreg: Register3 → 0Pop penalty, j, and i; add an implication constraint from variable i to variable j; raises IndexOutOfBounds outside [0, size).
0x74EQUALITYmodel: Register, indices: Register, coeffs: Register2 → 0Pop penalty and target; expand weighted equality constraint into QUBO terms on a model.
0x75ATLEASTmodel: Register, indices: Register2 → 0Pop penalty and k; allocate slack variables and apply at-least-k constraint.
0x76ATLEASTWmodel: Register, indices: Register, coeffs: Register2 → 0Pop penalty and k; allocate slack variables and apply weighted at-least-k constraint.
0x77REDUCEmodel: Register3 → 1Pop P_aux, var_b, var_a; allocate auxiliary variable and add Rosenberg enforcement terms; push aux index.
0x7FENERGYmodel: Register, sample: Register0 → 1Compute the Hamiltonian energy of a sample against a model; push the result.

Special

CodeMnemonicOperandsStackDescription
0xF0NOP0 → 0No operation.
0xFFHALT0 → 0Stop execution.

Bytecode Format

A .xqb file is the binary form of an XQVM program: a fixed 15-byte XQBC header followed immediately by the raw instruction stream. This page covers that wire format. For the human-readable .xqasm source format and how it compiles down to this, see Assembly.

The normative source is spec/xqvm/ENCODING.md. The Rust implementation of the header is Program::encode/Program::decode.

The XQBC header

OffsetWidthFieldDescription
0..44 bytesMagicThe ASCII bytes XQBC
41 byteVersionFormat version, currently 0x01
51 byteinput_slotsCount of INPUT instructions in the program (calldata arity)
61 byteoutput_slotsCount of OUTPUT instructions in the program
7..114 bytescode_lenByte length of the instruction stream, u32 big-endian
11..154 bytescrc32CRC-32/ISO-HDLC checksum of the instruction stream, u32 big-endian
15+Instruction streamRaw opcode and operand bytes

input_slots and output_slots are informational and are not validated by the decoder. Each counts instructions of that kind in the stream. Neither is a slot count: a program with three OUTPUT instructions that all write slot 0 records output_slots = 3 against a required slot count of 1, and a program with one OUTPUT inside a loop that writes slots 0 through 9 records output_slots = 1 against a required count of 10. Neither byte can therefore be used to pre-size a calldata or output-slot array. The host fixes both counts before the run – Vm::set_calldata and Vm::set_output_slots – and a slot outside them raises CallDataIndex or OutputIndex at run time. Both counts saturate at 255 (u8::MAX): a program with 300 INPUT instructions still encodes input_slots as 255, and there is no error path for the overflow. The count is also best-effort in another sense – it is produced by walking the instruction stream and skipping any instruction that fails to decode, so a malformed stream still yields a (possibly incomplete) count rather than aborting the walk.

A decoder rejects a file if any of the following hold:

  1. It is shorter than 15 bytes.
  2. Its first four bytes are not XQBC.
  3. Its version byte is not 0x01.
  4. The instruction-stream length does not match code_len.
  5. The CRC-32/ISO-HDLC of the instruction stream does not match crc32.

The instruction stream

Every instruction is an opcode byte followed by zero to eight operand bytes:

[ opcode : 1 byte ] [ operand bytes : 0-8 bytes ]

Instruction length is fixed per opcode – it is never encoded in the stream itself, so a decoder needs the opcode table, not a length prefix, to know how many operand bytes follow a given opcode byte. Register operands are a single u8 (0-255); PUSH1-PUSH8 operands are 1 to 8 bytes of big-endian signed two’s complement; label operands are a u8 (JUMP1/JUMPI1) or a u16 big-endian (JUMP2/JUMPI2). Multi-operand and multi-register opcodes concatenate their operands in the order the opcode table lists them – ENERGY r0 r1 encodes as 0x7F 0x00 0x01.

The opcode byte occupies 0x00-0x7F for the normal instruction space, plus two single-byte opcodes outside that range: 0xF0 (NOP) and 0xFF (HALT). Every other byte value in 0x80-0xFF, and any unassigned gap below 0x80, is rejected by the decoder as an unknown opcode.

TARGET and the label pre-scan

TARGET (0x00) has no operand – the opcode byte is the whole instruction. It marks a jump destination and does nothing at runtime; its only job is to exist at a fixed byte position so a decoder can find it.

A decoder builds an id-to-offset lookup by scanning the instruction stream once for TARGET opcodes: the first one encountered is assigned id 0, the second id 1, and so on in program order, each recorded against the byte offset where it starts. JUMP1/JUMP2/JUMPI1/JUMPI2 operands carry these sequential ids – not byte offsets, and not the .N label token that appears in .xqasm source. .N is assembler-only syntax used to resolve jump references before encoding; it is never emitted into the bytecode. In the Rust implementation this scan produces a JumpTable value (the JumpTable::scan associated function), and Program::new runs it once when a program is constructed from raw bytes.

This pre-scan is why a decoder cannot fully validate a single instruction in isolation: whether JUMP1 .3 is well-formed depends on how many TARGET opcodes exist anywhere in the program, which is only known once the whole stream has been scanned. See Verifier for how an out-of-range label is rejected before execution.

Instruction lengths by opcode

LengthOpcodes
1 byte (opcode only)TARGET, NEXT, RANGE, NOP, HALT, POP, SCLR, SWAP, COPY, ADD, SUB, MUL, DIV, MOD, SQR, ABS, NEG, MIN, MAX, INC, DEC, BITLEN, EQ, LT, GT, LTE, GTE, NOT, AND, OR, XOR, BAND, BOR, BXOR, BNOT, SHL, SHR, IDXGRID, IDXTRIU
2 bytes (opcode + 1)JUMP1, JUMPI1, LIDX, LVAL, ITER, LOAD, STOW, DROP, INPUT, OUTPUT, PUSH1, VEC, VECI, VECX, BQMX, SQMX, XQMX, BSMX, SSMX, XSMX, VECPUSH, VECGET, VECSET, VECLEN, GETLINE, SETLINE, ADDLINE, GETQUAD, SETQUAD, ADDQUAD, RESIZE, ROWFIND, COLFIND, ROWSUM, COLSUM, ONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, REDUCE
3 bytes (opcode + 2)JUMP2, JUMPI2, PUSH2, ENERGY, ATLEAST, SLACK
4 bytesPUSH3, EQUALITY, ATLEASTW
5 bytesPUSH4
6 bytesPUSH5
7 bytesPUSH6
8 bytesPUSH7
9 bytesPUSH8

XQMX and XSMX are exceptions worth flagging: each takes one register operand (2 bytes on the wire) but additionally pops two values off the value stack at runtime. Their bytecode length is 2, not 3 – the popped stack values never appear in the encoding.

Examples

InstructionBytes
NOP0xF0
HALT0xFF
TARGET0x00
PUSH1 420x11 0x2A
PUSH2 -10x12 0xFF 0xFF
LOAD r50x0A 0x05
JUMP1 .1000x01 0x64
JUMP2 .10000x03 0x03 0xE8
JUMPI1 .50x02 0x05
ENERGY r0 r10x7F 0x00 0x01
BQMX r20x40 0x02

The PUSHn rows show the encoded instruction, not source you can type: the assembler accepts the PUSH <value> sugar (and its PUSHC alias) and selects the width itself. The JUMPn/JUMPIn forms are typeable but interact badly with the unused-label check – see Control Flow. Write JUMP/JUMPI and let the assembler pick the width.

Where this is implemented

Verifier

The verifier performs static analysis over a program before it runs, so structural and semantic errors are caught at load time rather than partway through execution. It runs as a fixed pipeline of phases; each phase makes one pass over the program and the pipeline is fail-fast, stopping at the first violation any phase finds. A program with two independent defects reports only the first one encountered.

The Rust implementation is xqvm::verifier, exposed to Python via xqffi.verifier and re-exported as xquad.verifier. There is one implementation; xqvm_py, the pure-Python reference VM, does not verify.

This page is the reference: what each phase checks and what each error means. For why a specific program was rejected and how to change it, see Verification.

What passing verification guarantees

Verifier::default() builds the standard pipeline described below. Calling its run method against a program returns Ok(()) if every phase passes, or the first VerifierError encountered otherwise. A program that passes is guaranteed structurally sound: every opcode decodes, every jump lands on a real TARGET, every loop is balanced, and every register is read only after it is written on every reachable path. For the value stack, phase 4 found no underflow, no join-point depth disagreement, and no basic block whose depth exceeds the 8,192-item limit. That analysis models each basic block as a net stack delta, so an instruction that pops more operands than it pushes has its pop requirement absorbed whenever the running depth stays non-negative – a stack underflow caused by an operand-ordering error can still pass verification.

Every phase reasons about types, control flow and depths. None reasons about values, so no phase can decide a question whose answer the program computes at run time. An allocator size, a grid extent, a loop bound, a shift amount, a calldata or output index and every arithmetic operand are all ordinary popped stack values, which is why InvalidAllocation, InvalidGridDimensions, InvalidIntegerK, InvalidShift, ArithmeticOverflow, IndexOutOfBounds, SampleOutOfDomain, LoopStackOverflow and the two budget faults exist only at runtime and have no verifier counterpart. SampleOutOfDomain is the clearest of them: SETLINE on a sample is well-typed whatever it writes, and only the value the program computes decides whether the write is in domain. That is a boundary rather than a gap: an embedder gets its bound from the step and allocation budgets it sets, not from a verification pass.

Pipeline

OrderPhaseChecks
1aStructuralTruncated instruction bytes, unknown opcodes
1bJump targetA jump label id at or past the program’s target count
1cLoop nestingRANGE/ITER/NEXT balance, loop-only instructions used outside a loop
2Register type-stateControl-flow-based read-before-write and register type checks
3Must-initControl-flow AND-meet analysis: a register read that is unset on at least one incoming path
4Stack depthControl-flow-based stack underflow, overflow risk, and join-point depth mismatches

Phases 1a-1c share one linear scan over the instruction bytes – the same scan that produces the program’s id-to-offset lookup for TARGET labels (see Bytecode Format). Phases 2 through 4 share one control-flow graph of basic blocks, built once, and each runs its own forward worklist analysis over it. The control-flow graph correctly accounts for unreachable code, conditional branches, and loop back-edges, catching errors that a purely linear scan would miss.

Phase 1 – structural, jump target, loop nesting

1a, structural. Every byte in the instruction stream must decode to a known opcode with the correct number of operand bytes. An unrecognised opcode byte or a truncated operand sequence is rejected immediately. Errors: TruncatedInstruction, BadOpcode.

1b, jump target. Every JUMP1, JUMPI1, JUMP2, and JUMPI2 instruction must reference a label id produced by the TARGET pre-scan; an id greater than or equal to the program’s target count is invalid. Error: UndefinedJumpTarget.

1c, loop nesting. RANGE and ITER each open a loop frame; NEXT closes one. The verifier rejects NEXT, LVAL, or LIDX reached with no open loop frame, and any loop frame still open at the end of the program (reported at the byte offset of the outermost unmatched opener). Errors: NoActiveLoop, UnmatchedLoop.

Phase 2 – register type-state

A control-flow-based forward analysis over 256 register slots. Each register starts Unset. An instruction that writes a register advances its slot to a concrete type (Int, VecInt, VecXqmx, Model, Sample) or to Any; an instruction that reads a register is checked against what that instruction requires.

At a join point – two or more incoming control-flow paths – the per-register meet is permissive: disagreeing types, including a register unset on one path and Int on the other, resolve to Any rather than an immediate error. That permissiveness avoids false positives for a register written on only one branch of a conditional; Phase 3 is what actually catches that case. Reading an Unset register is rejected, and so is a type mismatch, for example reading a Model register where an instruction requires Int.

A join is held back from that permissiveness, because Any satisfies every requirement. Joining a model with a sample gives model or sample, and joining the two vector types gives vec<int> or vec<xqmx>; each satisfies only what its two members have in common – the grid operations and the linear coefficient trio for the first, ITER and VECLEN for the second. Any other pair of types gives conflicting types, which satisfies nothing but a read that accepts any set register, such as OUTPUT. A program that allocates a model on one branch and a sample on the other and then writes a quadratic coefficient is rejected, which is the point: it would fault at run time on one of its two paths. Answering unrelated pairs this way is also what keeps the result the same however many branches meet, and whatever order they are in.

DROP is a special case worth knowing. At runtime it resets the register to Unset, and the verifier models that faithfully: DROP resets the register’s tracked type to Unset, so a read after DROP is rejected as ReadUnsetRegister and would also fault at runtime as UnsetRegister. Unreachable blocks are skipped, so dead code after an unconditional jump never produces a false positive.

Errors: ReadUnsetRegister, RegisterTypeMismatch.

Phase 3 – must-init analysis

A control-flow-based forward analysis using an AND-meet over a bitmap of which registers are definitely initialised. At program entry every register is un-written. At a join point, a register’s bit is cleared unless every incoming path wrote it – so a register written on only one branch of a conditional is flagged as possibly-unset at any read after the join, even though Phase 2’s permissive meet let the same register pass its type check. Unreachable blocks are skipped.

Error: ReadUnsetRegister.

Phase 4 – stack depth

A control-flow-based forward analysis over the abstract depth of the value stack. Each basic block is characterised by the minimum depth it needs on entry and its effect on exit; the analysis propagates the most conservative depth to each successor.

Checks run in this order:

  1. Loop stack imbalance. A separate pass walks each loop body, ignoring the back-edge, and compares the depth at NEXT against the depth at the matching RANGE/ITER opener. A non-zero net effect is an imbalance, reported before anything else in that loop is checked.

  2. Stack depth mismatch. After the pass converges, every join block – two or more incoming edges – has the arrival depths of all its reachable incoming edges compared; any two that differ mean the stack state at that point is not well-defined.

    Program entry counts as an incoming edge of the entry block, arriving at depth 0. So a back-edge targeting the entry block makes it a join block even though it has only one predecessor block, and the loop body’s exit depth is compared against entry’s depth 0.

  3. Underflow and overflow risk. A block whose converged depth would fall below zero, or exceed 8,192, is flagged. The block’s entry requirement is a running minimum that rises only when the depth within the block goes negative: a pop requirement absorbed by pushes earlier in the same block never makes the running depth negative, so it is invisible to this check.

SCLR resets the abstract depth unconditionally to zero, which the analysis treats as a reset rather than a delta. If the entry depth was N > 0 and the exit depth is 0, that reset is itself the imbalance: inside a loop it is reported directly as LoopStackImbalance by check 1, ahead of everything else in that loop. Outside a loop, the same reset is only a problem if it creates a discrepancy between incoming edges at a join, in which case check 2 catches it as StackDepthMismatch.

Errors: LoopStackImbalance, StackDepthMismatch, StackUnderflow, StackOverflowRisk.

Error reference

ErrorPhaseMeaning
TruncatedInstruction1aThe instruction stream ends mid-operand
BadOpcode1aAn opcode byte does not map to a known instruction
UndefinedJumpTarget1bA jump references a label id at or past the program’s target count
NoActiveLoop1cNEXT, LVAL, or LIDX reached with no open loop frame
UnmatchedLoop1cA loop frame is still open at the end of the program
ReadUnsetRegister2 or 3A register is read before it is written, or before it is written on every path
RegisterTypeMismatch2A register holds a different RegVal variant than the instruction expects
LoopStackImbalance4A loop body’s entry and exit stack depths differ
StackDepthMismatch4Two control-flow paths reach a join point with different stack depths
StackUnderflow4Stack depth would go negative at a reachable instruction
StackOverflowRisk4Stack depth would exceed 8,192 at a reachable instruction

Every VerifierError variant carries the byte offset of the offending instruction; several (RegisterTypeMismatch, UndefinedJumpTarget, and others) also carry the register number or label id involved, so a diagnostic can point at the exact spot in the bytecode.

When a program fails

A verifier failure is a load-time rejection: nothing in the program has executed. xquad verify runs the pipeline from the command line without running the program; see CLI. For what to change in your source when a specific error fires, see Verification, which covers the same errors from the side of fixing them rather than defining them.

Sources

CLI Reference

xquad is the unified command-line driver for the XQVM toolchain. It is built by the xqcli crate, but the crate and the binary have different names: cargo install xqcli installs a command called xquad, not xqcli. Running xqcli --help after installing fails because no such binary exists.

Installation

cargo install xqcli
xquad --help

Or build from this repository:

cargo build -p xqcli --release

The binary is at target/release/xquad.

Subcommands

xquad has four subcommands, one per stage of the toolchain:

CommandDescription
xquad asmAssemble .xqasm source into binary bytecode.
xquad dismDisassemble bytecode into a human-readable listing.
xquad runExecute bytecode or assembly, with optional tracing.
xquad verifyRun the bytecode verifier over a program without running it.

General usage

xquad <COMMAND> [OPTIONS] [ARGS]
xquad --help
xquad <COMMAND> --help

xquad asm reads .xqasm source and writes bytecode; it has no --text flag and rejects a .xqb file. xquad dism reads bytecode from a file or stdin, also with no --text flag. xquad run and xquad verify read bytecode by default and read .xqasm assembly instead when --text is passed.

Error reporting is not uniform across the four subcommands. Only xquad asm propagates the real xqasm::Error, with its NamedSource and SourceSpan, so only asm prints a highlighted source snippet pointing at the failing token. xquad run --text and xquad verify --text both flatten an assembly failure through miette::miette!("{e}"), which formats the error to a plain string and discards the span; xquad verify on a .xqb file flattens a verifier failure the same way. Decode errors (a malformed .xqb file) go through .into_diagnostic(), which also carries no span. So an assembly, verification or decode failure reaches you as a bare message – see xquad verify for what that looks like in practice. A VM runtime fault under xquad run is the one exception: it goes through into_diagnostic(), which disassembles the program around the failing offset and annotates it, so that path does print a snippet. Argument-parsing failures, such as asm’s -o/--stdout conflict, are plain clap errors, a third and unrelated error path.

A minimal round trip

cat > add.xqasm <<'EOF'
; add.xqasm -- push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
EOF

xquad asm add.xqasm -o add.xqb
xquad verify add.xqb
xquad run add.xqb

xquad run add.xqb prints the residual stack:

stack (bottom to top):
  42

xquad asm

Assemble an .xqasm source file into binary XQVM bytecode.

Usage

xquad asm <INPUT> [-o <OUTPUT>] [--stdout]

Arguments

ArgumentDescription
INPUTPath to the assembly source file.

Options

OptionDefaultDescription
-o, --output <OUTPUT><INPUT> with its extension replaced by .xqbOutput file path.
--stdoutoffWrite bytecode to stdout instead of a file. Conflicts with -o/--output.

-o and --stdout are mutually exclusive; passing both is a clap argument error, not a runtime one:

xquad asm add.xqasm -o build/program.xqb --stdout
error: the argument '--output <OUTPUT>' cannot be used with '--stdout'

Usage: xquad asm --output <OUTPUT> <INPUT>

For more information, try '--help'.

Examples

Assemble to the default output

xquad asm add.xqasm

Writes add.xqb next to the source and prints a summary to stderr:

assembled 4 instructions (21 bytes) -> add.xqb

Assemble to a specific output file

mkdir -p build
xquad asm add.xqasm -o build/program.xqb

Assemble does not run the verifier

xquad asm only assembles; it does not check the result with xquad verify. A source file with no mnemonic errors can still fail verification once assembled.

Pipe bytecode to another tool

xquad asm add.xqasm --stdout | xquad dism
  0x0000:  PUSH1   10
  0x0002:  PUSH1   32
  0x0004:  ADD     
  0x0005:  HALT    

Output

On success, prints a summary to stderr (suppressed with --stdout, since the bytecode itself occupies stdout). The instruction count includes every TARGET the assembler emits, so a program with jumps counts higher than its visible mnemonics. branch.xqasm below has eight mnemonic lines and two labels (.0, .1), each of which emits a TARGET:

PUSH 5
PUSH 10
GT
JUMPI .0
PUSH 99
JUMP .1
.0: PUSH 0
.1: HALT
xquad asm branch.xqasm -o branch.xqb
assembled 10 instructions (31 bytes) -> branch.xqb

Error reporting

Assembly errors include the source file location and a highlighted snippet:

Error: xqasm::unknown_mnemonic

  × unknown mnemonic `BADOP`
   ╭─[bad.xqasm:2:1]
 1 │ PUSH 1
 2 │ BADOP r0
   · ──┬──
   ·   ╰── unknown mnemonic
 3 │ HALT
   ╰────

xquad dism

Disassemble XQVM bytecode into a human-readable listing.

Usage

xquad dism [FILE]

Arguments

ArgumentDescription
FILEBytecode file to disassemble. Reads from stdin when omitted.

dism decodes FILE (or stdin) as a full XQBC container, the same format xquad asm writes: a 15-byte header (magic, version, code length, CRC32) followed by the raw instruction stream. It has no path for a bare instruction stream missing that header – a buffer that starts straight with opcode bytes fails to decode on the magic check before disassembly gets a chance to run. This is why the stdin example below pipes from xquad asm --stdout, which emits a full container, rather than from a raw instruction buffer.

Examples

Disassemble a file

xquad dism add.xqb
  0x0000:  PUSH1   10
  0x0002:  PUSH1   32
  0x0004:  ADD     
  0x0005:  HALT    

Disassemble from stdin

xquad asm add.xqasm --stdout | xquad dism

Produces the same listing as above; dism reads the bytes either way.

A program with jumps

branch.xqb, assembled from a program with two labels (.0, .1) and two jumps:

  0x0000:       PUSH1   5
  0x0002:       PUSH1   10
  0x0004:       GT      
  0x0005:       JUMPI1  .0
  0x0007:       PUSH1   99
  0x0009:       JUMP1   .1
  0x000B:  .0:  TARGET  
  0x000C:       PUSH1   0
  0x000E:  .1:  TARGET  
  0x000F:       HALT    

Output format

Each line shows a byte offset, an optional label, and a decoded instruction:

  • Byte offset (0x0000:) – position in the instruction stream.
  • Label column – present only when the program contains at least one TARGET. When present, it holds .0:, .1:, and so on at the offset where each TARGET opcode sits, and is blank on every other line so the instruction column still lines up. When the program has no TARGET at all, as in add.xqb above, the column is omitted entirely rather than printed blank on every line.
  • Instruction – mnemonic and decoded operands. JUMP/JUMPI operands are printed as the same .N label that marks their destination, not as a raw byte offset or an internal id.
  • PUSH values – shown as sign-extended decimal integers (PUSH1 10, not a hex byte).
  • Undecodable bytes – an invalid opcode or a truncated operand does not abort the listing. It is rendered as a .byte 0xNN pseudo-instruction and the walk continues from the next byte, so no byte is silently dropped from the output.

The label numbering comes from a single left-to-right scan over the decoded program: the first TARGET found is .0, the second is .1, and so on. This is the same numbering the assembler resolves .0/.1-style source labels against, so a disassembly’s labels read back as valid .xqasm jump targets – provided the listing contains no .byte lines. .byte is not a recognised mnemonic, so a listing that hit an undecodable byte is not itself valid .xqasm input, even though its labels are correctly numbered. See Bytecode Format for how that scan works and why a JUMP operand cannot be validated in isolation from it.

xquad run

Execute XQVM bytecode or assembly source, printing the residual outputs and stack, with optional step-by-step tracing.

Usage

xquad run [OPTIONS] <FILE>

Arguments

ArgumentDescription
FILEBytecode (.xqb) file to run, or assembly (.xqasm) source when --text is set.

Options

OptionDefaultDescription
--textoffTreat FILE as assembly source and assemble it before running.
--calldata <CALLDATA>noneComma-separated i64 integers passed to INPUT instructions.
--outputs <OUTPUTS>16Number of output slots available for OUTPUT instructions.
--step-limit <STEP_LIMIT>10000000Maximum number of instructions to execute. The limit is exact: 0 executes nothing. Conflicts with --unlimited-steps. See Step limits.
--unlimited-stepsoffRun without a step limit. Conflicts with --step-limit. A program that never halts will not return.
--memory-limit <MEMORY_LIMIT>1073741824Allocation budget in bytes for models, samples and vectors. See The allocation budget.
--traceoffEnable step-by-step execution tracing.
--trace-format <TRACE_FORMAT>textTrace output format: text or json. Requires --trace.
--trace-file <TRACE_FILE>stderrWrite trace output to a file instead of stderr. Requires --trace.

xquad run does not call the verifier. A program that fails verification can still be handed to run; some defects (an unset register read, for example) are also caught by the VM’s own runtime checks and abort with a runtime error, but others are not. See xquad verify and Verification for what static verification does and does not guarantee.

Examples

Run bytecode

xquad run add.xqb
stack (bottom to top):
  42

Run assembly directly

xquad run --text add.xqasm

Skips the separate xquad asm step; the source is assembled in memory and run immediately.

Pass calldata and read outputs

INPUT and OUTPUT each take one register operand in assembly, but both pop the slot index off the value stack at runtime – the index is not implicit in the instruction. A program that reads calldata[0] into r0, adds one, and writes the result to output slot 0 needs the index pushed explicitly before each call: PUSH 0; INPUT r0; ...; PUSH 0; OUTPUT r1; HALT.

xquad run io.xqb --calldata 41
outputs:
  [0] = Int(42)

--calldata accepts a comma-separated list; each value is consumed in order by successive INPUT instructions. OUTPUT faults UnsetRegister if the register it reads was never written, the same as LOAD. Both INPUT and OUTPUT fault with CallDataIndex/OutputIndex respectively if the popped index is out of range for the calldata or output-slot array – the index itself is not guaranteed to be valid just because it came off the stack. See Register I/O for the full set of INPUT/OUTPUT error modes.

Enable tracing

xquad run regs.xqb --trace

Trace lines go to stderr:

step    offset    instruction            stack                      read-regs        written-regs   
     1  0x0000    PUSH1 7                [7]                                                        
     2  0x0002    STOW r0                []                                          r0=7           
     3  0x0004    LOAD r0                [7]                        r0=7                            
     4  0x0006    HALT                   [7]                                                        

and the residual stack goes to stdout, separately from the trace:

stack (bottom to top):
  7

Trace lines go to stderr by default so the stdout result is not interleaved with them in a terminal. Redirect stderr separately to capture the trace on its own:

xquad run regs.xqb --trace 2>trace.txt

JSON trace

xquad run regs.xqb --trace --trace-format json --trace-file trace.jsonl

trace.jsonl then holds one JSON object per step:

{"step":1,"pos":0,"instruction":"PUSH1 7","stack":[7],"read_regs":{},"written_regs":{}}
{"step":2,"pos":2,"instruction":"STOW r0","stack":[],"read_regs":{},"written_regs":{"0":{"type":"int","value":7}}}
{"step":3,"pos":4,"instruction":"LOAD r0","stack":[7],"read_regs":{"0":{"type":"int","value":7}},"written_regs":{}}
{"step":4,"pos":6,"instruction":"HALT","stack":[7],"read_regs":{},"written_regs":{}}

Custom step limits

countloop.xqb assembles a five-iteration RANGE loop around a NOP: six encoded instructions, 14 executed steps, because the body and its NEXT run once per iteration. A limit that is reached before HALT aborts the run:

xquad run countloop.xqb --step-limit 3
Error: xqvm::runtime_error

  × step limit of 3 exceeded

The limit is exact, and 0 is not a sentinel for “unlimited”. A zero budget permits no instructions at all, so the first fetch fails:

xquad run countloop.xqb --step-limit 0
Error: xqvm::runtime_error

  × step limit of 0 exceeded

Passing --unlimited-steps alongside --step-limit is rejected by the argument parser rather than resolved by precedence, so opting out of the bound cannot be said two ways at once:

xquad run countloop.xqb --step-limit 0 --unlimited-steps
error: the argument '--step-limit <STEP_LIMIT>' cannot be used with '--unlimited-steps'

The conflict fires on an explicit --step-limit, not on the default, so --unlimited-steps on its own is accepted. It removes the bound entirely, which is only safe where you control the program or can abandon the thread – a program that never halts will not return:

xquad run countloop.xqb --unlimited-steps

prints nothing, since the program leaves no outputs and no residual stack. To raise the limit rather than remove it, pass a larger number, up to u64::MAX:

xquad run countloop.xqb --step-limit 18446744073709551615

also runs to completion, but for a different reason: the limit is higher, not absent. A budget that exactly covers the program succeeds – --step-limit 14 runs countloop.xqb to completion, and --step-limit 13 does not.

Output

After execution, xquad run prints, in order:

  1. Outputs – every output slot that was written, with its index and value.
  2. Stack – any values remaining on the value stack, bottom to top.

A run that both writes outputs and leaves values on the stack prints both sections:

outputs:
  [0] = Int(42)
  [1] = VecInt([1, 2, 3])
stack (bottom to top):
  7

Error reporting

Runtime errors print the faulting instruction with a disassembled context listing around it. underflow.xqb is PUSH 1 / ADD / HALT: ADD needs two stack values and only one was pushed.

xquad run underflow.xqb
Error: xqvm::runtime_error

  × stack underflow at byte 0x0002
   ╭─[underflow.xqb:2:1]
 1 │   0x0000:  PUSH1   1
 2 │   0x0002:  ADD     
   · ─────────┬─────────
   ·          ╰── execution failed here
 3 │   0x0003:  HALT    
   ╰────

xquad verify

Run the bytecode verifier over a program without executing it, and report the first violation found.

Usage

xquad verify [OPTIONS] <FILE>

Arguments

ArgumentDescription
FILEProgram to verify: bytecode (.xqb) by default, assembly (.xqasm) when --text is set.

Options

OptionDefaultDescription
--textoffTreat FILE as assembly source and assemble it before verifying.

xquad verify runs the same default pipeline documented in Verifier: structural checks, jump-target checks, loop-nesting balance, register type-state, must-init analysis, and stack depth, in that order, stopping at the first phase that fails. This page covers only the subcommand’s own behaviour; for what each phase checks and what each error means, see that page. For a task-oriented walkthrough of fixing a rejected program, see Verification.

A passing program

xquad verify add.xqb
ok: add.xqb (4 instructions)

The instruction count includes the whole decoded stream: labelled positions as well as visible mnemonics. verify writes this line to stdout and exits 0. With --text, the same message names the .xqasm source path rather than a .xqb file, since the source is assembled in memory first and never written to disk.

A failing program

uninit.xqasm reads register r0 before anything ever writes it:

LOAD r0
HALT
xquad verify --text uninit.xqasm
Error:   × register r0 read at byte 0x0000 before being written

verify exits 1 on the first violation any phase finds. A program with two independent defects reports only this one; fixing it and re-running may surface the next.

Passing verification is not a runtime guarantee

Verification is a set of static, per-basic-block checks. The stack-depth phase sees each block’s net stack effect, so an instruction that pops more operands than it pushes has its pop requirement absorbed by earlier pushes. idx_underflow.xqasm pushes two values and then runs IDXGRID, which pops three:

PUSH 1
PUSH 2
IDXGRID
HALT
xquad verify --text idx_underflow.xqasm
ok: idx_underflow.xqasm (4 instructions)

xquad run --text idx_underflow.xqasm then underflows the value stack at the IDXGRID:

Error: xqvm::runtime_error

  × stack underflow at byte 0x0004
   ╭─[idx_underflow.xqasm:3:1]
 2 │   0x0002:  PUSH1   2
 3 │   0x0004:  IDXGRID 
   · ─────────┬─────────
   ·          ╰── execution failed here
 4 │   0x0005:  HALT    
   ╰────

Nor does verification look at values. An allocator size, a grid extent, a loop bound and every arithmetic operand are runtime quantities, so a program that allocates a negative model, resizes past its own variable count, overflows an i64 or exhausts its step or allocation budget verifies cleanly and faults when run. See Verifier for the precise scope of what a pass proves, and Limits and Errors for the faults that have no static counterpart.

Limits and Errors

XQVM has two separate error surfaces. The verifier is static analysis that runs before a program executes; xquad verify runs it without running the program. The VM is runtime: xquad run does not verify automatically, so a program that was never checked can reach the VM and fault there instead. Several conditions – an out-of-range jump label, in particular – have both a verifier error and a distinct VM runtime error for this reason.

Fixed Limits

LimitValueEnforced by
Stack depth8,192 itemsVM: StackOverflow. Verifier: StackUnderflow/StackOverflowRisk (Phase 4)
Register count256 slots (r0-r255), statically allocated
Jump label range0-65,535 (u16), 65,536 labels maxAssembler: TooManyTargets. Verifier: UndefinedJumpTarget. VM: InvalidLabel
Shift amount0-63 bitsVM: InvalidShift
Grid dimensionsrows and cols must be > 0, and rows * cols must not exceed the register’s declared sizeVM: InvalidGridDimensions
XQMX size4,294,967,295 (2^32 - 1) variables, and the allocation budgetVM: InvalidAllocation past the maximum, MemoryLimitExceeded when the budget cannot pay. Applies to a size an allocator was given and to one ATLEAST, ATLEASTW, REDUCE or EQUALITY grew a model to
Loop nesting8,192 framesVM: LoopStackOverflow
Integer domain size (XQMX/XSMX)k >= 2VM: InvalidIntegerK
Sample assignment valuea member of the register’s domain: {0, 1} binary, {-1, +1} spin, {0, ..., k-1} integerVM: SampleOutOfDomain on SETLINE/ADDLINE. Model coefficients are unbounded and never raise it

Configurable Limits

LimitLibrary defaultMethodVM error when exceeded
Step budget10,000,000Vm::set_step_limit(n) / Vm::set_unlimited_steps()StepLimitExceeded
Allocation budget1 GiBVm::set_memory_limit(bytes)MemoryLimitExceeded
Calldata slots0Vm::set_calldata(vec)CallDataIndex
Output slots0Vm::set_output_slots(n)OutputIndex

The library defaults above apply to Vm::new() directly; the xquad run CLI sets its own defaults before handing control to the VM – 16 output slots unless --outputs overrides it. See xquad run for the CLI’s own default table.

The step limit is exact: set_step_limit(0) permits no instructions at all, not unlimited ones. To remove the bound, call Vm::set_unlimited_steps(). Nothing else removes it, and nothing removes it by default. Until 0.4.0 0 was the sentinel for “unlimited”, which made a zero budget the most dangerous value a caller could pass rather than the safest.

The step budget

A step is a unit of metered execution cost, not an instruction: every instruction charges a base cost before dispatch, and opcodes whose work scales with data the program controls – evaluating a model, expanding a constraint, copying a register that holds a model – charge more before they do that work. An instruction that cannot pay the charge fails with StepLimitExceeded and does none of the work it would have charged for. Vm::steps() reports the metered total; Vm::instructions() reports the plain dispatch count, which is always less than or equal to steps(). See spec/xqvm/METERING.md for the full cost model, including the per-opcode charges and the constants they use.

The allocation budget

Every instruction that allocates a sample buffer, declares model variables, grows a vector, or expands constraint coefficients is charged against the budget before it allocates. An instruction that cannot pay fails with MemoryLimitExceeded and allocates nothing, leaving its target register untouched. Vm::memory_used() reports what a run spent.

Charges are cumulative rather than a high-water mark of live memory: bytes are charged when they are allocated and are never refunded, so a loop that allocates and discards cannot spend more than the budget in total. Each run() starts from zero. There is no sentinel for “unlimited” – pass u64::MAX.

ChargedRate
BQMX, SQMX, XQMX (declared model size)8 bytes per variable
BSMX, SSMX, XSMX (sample buffer)8 bytes per variable
VECPUSH, SLACK (vector growth)16 bytes per element
SETLINE, ADDLINE on a model32 bytes per coefficient
SETQUAD, ADDQUAD, EXCLUDE, IMPLIES48 bytes per coefficient
ONEHOTR, ONEHOTC, EQUALITY, ATLEAST, ATLEASTWworst-case expansion: one linear term per variable and one quadratic term per pair
REDUCEone auxiliary variable, three quadratic terms, one linear term
ITER on a vec<int> (slice copied into the loop frame)8 bytes per element
ITER on a vec<xqmx> (slice copied into the loop frame)one whole-model copy per element – see below

Models store their coefficients sparsely, so a declared model size costs nothing immediately; it is charged because every consumer of the model – the sample needed to evaluate it, each solver backend – has to materialise it. The expanding constraint opcodes are charged for their worst case, so an expansion whose indices collide can be charged more than it ultimately stores.

VEC, VECI and VECX install an empty vector and allocate nothing; their storage is charged as VECPUSH and SLACK create it.

ITER copies the slice it iterates into its loop frame, and a loop frame is released only by NEXT. A back-edge that re-enters an ITER without reaching its NEXT therefore accumulates copies, which is why the copy is charged. The loop frame itself is not charged, but it is bounded: the loop stack is capped at 8,192 frames and a program that grows it past that fails with LoopStackOverflow.

An element of a vec<xqmx> is charged the whole-model copy rate, because cloning a model clones its coefficient maps: 8 bytes per declared variable plus 32 per live linear coefficient and 48 per live quadratic one. That is exactly what the allocator and the coefficient writes charged to build the model in the first place, and it is the same number wherever the copy happens – through an ITER, or through INPUT/OUTPUT copying a whole register across the host boundary. A model does not get cheaper by being duplicated through one opcode rather than another.

The charge schedule is defined over program-visible quantities – variables declared, elements appended, coefficients written – rather than over either interpreter’s internal representation. The Python reference interpreter charges the same rates via Executor.execute(..., memory_limit=...), raising xqvm_py.errors.MemoryLimitExceeded, so both implementations reject the same programs at the same instruction having charged the same bytes. xquad.vm.VM.set_memory_limit() sets it on either backend and VM.memory_used() reads the result back.

VM runtime errors

Produced by xqvm::Error (xqvm/src/error.rs) while a program is executing. Every variant that carries a byte position (pos) can be turned into a RuntimeDiagnostic via Error::into_diagnostic, which disassembles the program and points at the failing instruction.

ErrorCause
StackUnderflowPopping from an empty or too-shallow stack
StackOverflowPushing when the stack is already at 8,192 items
RegisterTypeInstruction expects a different RegVal variant than the register holds
IncompatibleTypeType mismatch reported without register context
UnsetRegisterLOAD or OUTPUT on a register that was never written, or was DROPped
DivisionByZeroDIV or MOD with divisor 0
ArithmeticOverflowAn i64 operation left the signed 64-bit range. Every operation the VM performs on a program’s behalf is checked, intermediates included, so a computation whose mathematical answer is representable still faults when a partial result is not
IndexOutOfBoundsAn index operand outside what it addresses: a vec index, a coefficient index against the model’s declared size, or a grid row or column against the extent RESIZE declared
NoActiveLoopNEXT, LVAL, or LIDX with no active loop
BadJumpTargetJump target lands outside the bytecode buffer
InvalidLabelJUMP/JUMPI references a label id the id-to-offset scan never resolved
BadOpcodeUnrecognised opcode byte
TruncatedInstructionBytecode ends mid-instruction
CallDataIndexINPUT index out of range
OutputIndexOUTPUT index out of range
SizeMismatchENERGY sample length does not match model size
VecLengthMismatchTwo parallel vectors used together (for example EQUALITY’s indices and coefficients) have different lengths
StepLimitExceededA step charge could not be paid: "step charge of {requested} exceeds the step limit of {limit} ({used} steps already charged)", where requested is 1 for the base per-instruction cost or the larger charge an opcode asked for before doing data-scaled work
MemoryLimitExceededAn allocating instruction exceeded the configured allocation budget
InvalidShiftSHL/SHR shift amount outside [0, 64)
InvalidGridDimensionsRESIZE with rows or cols <= 0, RESIZE with rows * cols past the register’s declared size, or a grid-reading opcode on a register with no grid
InvalidAllocationAn allocator given a negative size, or one past the maximum allocator size 2^32 - 1; also a constraint that would grow a model past it, since ATLEAST, ATLEASTW, REDUCE and EQUALITY all append variables. Both are properties of the operand rather than of the machine running it, so the same size is refused everywhere. Under an ordinary budget an oversized size raises MemoryLimitExceeded first, since the charge precedes the range check
LoopStackOverflowRANGE/ITER nesting past 8,192 frames
InvalidIntegerKXQMX/XSMX called with k < 2k counts the values in {0, ..., k-1}, so k = 1 leaves a single value and no decision to make
SampleOutOfDomainSETLINE/ADDLINE wrote a value outside a sample’s domain. ADDLINE checks the result of the addition rather than the delta. Sample registers only: a model’s linear[i] is a bias, not an assignment, and is unbounded
UnmatchedLoopA RANGE/ITER skip-forward scan reached the end of the stream without a matching NEXT
TraceFailedA tracer callback returned an error (for example an I/O write failure)

Verifier errors

Produced by xqvm::verifier::VerifierError (xqvm/src/verifier/error.rs) before a program runs, by the four-phase pipeline described in Verifier: structural, jump-target and loop-nesting checks; register type-state; must-init analysis; and stack depth.

The eleven variants, which phase raises each one, and what each one means are catalogued in Verifier’s error reference rather than repeated here, since that page also explains the phase that produces each one. See Verification for how to fix a rejected program.

Substrate pallet fixture limits

fixtures/pallet-xqvm (excluded from the main Cargo workspace build; see Embedding Overview) adds two additional bounds on top of the ones above, enforced by the runtime’s Config trait rather than the VM:

LimitConfig itemPurpose
Program sizeMaxProgramSizeMaximum bytecode byte length accepted by submit_program
Calldata and output countMaxCalldataShared bound on both the calldata vector and the output-slot vector

submit_program takes the step budget as an extrinsic argument rather than reading a pallet constant, so a caller names the bound the VM may spend. The bound is exact, and a step_limit of 0 fails rather than succeeding vacuously. The fixture sets no allocation budget, so the VM’s 1 GiB default applies – far too generous for a runtime that has to price what it admits. See Substrate Pallet.

Embedding Overview

Everything so far in this book drives XQuad from Python or the xquad CLI. This chapter is for embedding the Rust crates directly. That means building bytecode with a fluent Rust API instead of .xqasm text, running it without a Python process in the loop, and compiling for no_std targets.

Crate Map

CrateRoleno_std
xqvmOpcode table, InstructionBuilder, the wire codec, and the Vm interpreterYes, with --no-default-features
xqasmParses .xqasm text into an xqvm::ProgramNo
xqcliBuilds the xquad CLI binary on top of the two crates aboveNo

A Rust program that only needs to build and run bytecode depends on xqvm alone; add xqasm to parse .xqasm source instead of building bytecode by hand with InstructionBuilder. xqcli is the CLI’s own crate, useful as a reference for wiring the other two together rather than as a library dependency.

The no_std Story

xqvm builds no_std + alloc. Two layers are always compiled without std:

  • Bytecode – the opcode table, Instruction, Register, InstructionBuilder, Program, and the wire codec.
  • Interpreter coreVm, Error, RegVal, XqmxModel / XqmxSample, the bytecode verifier, and the Tracer trait with its no-op implementation.

The std feature, enabled by default, adds three things a no_std embedder gives up: the disassembler (disasm::Disassembly), miette-backed runtime diagnostics (RuntimeDiagnostic), and the JsonTracer / TextTracer implementations. None of these change what a program computes; they change how a fault or a trace is reported. Build for a no_std target with:

cargo add xqvm --no-default-features

This is exercised, not just claimed: fixtures/xqvm-wasm is a freestanding wasm32-unknown-unknown test crate depending on xqvm with default-features = false, and CI’s test:wasm job runs cargo build -p xqvm --target wasm32v1-none --no-default-features followed by wasm-pack test --node against it. The job is path-gated: it runs on merge requests and feature branches whenever the diff can reach xqvm or the fixture, and unconditionally on protected refs and release tags.

Running In-Process, Without .xqasm

Builder API covers InstructionBuilder in full: emitting instructions one at a time, resolving forward and backward labels, and reading back the resulting JumpTable. It is the API a no_std embedder uses when parsing text is not an option.

On-Chain: The Pallet Fixture

Pallet Fixture documents fixtures/pallet-xqvm, a Substrate FRAME pallet that embeds xqvm inside a runtime as an integration gate, not a production deployment path. It is a real, CI-tested example of a no_std embedder, and the one currently in this repository.

On-Chain: Which Opcodes a Chain May Admit

On-Chain Admissibility answers which of the 93 opcodes a runtime embedding xqvm may accept from an untrusted account. The answer is all of them, because chain compatibility is a property of the instruction set rather than a per-embedder decision; the page states the bar an opcode has to clear, where each half of it is enforced, what every family charges, and which parts of that boundary the tree does not yet enforce.

Checking an Implementation Against the Spec

Conformance covers the harness that holds the Rust xqvm and the Python xqvm_py to the same observable behaviour. Read it if you are embedding xqvm somewhere that needs to trust its output matches the reference implementation.

Builder API

InstructionBuilder is a fluent Rust API for constructing XQVM bytecode programmatically, without going through the text assembler. It lives in the xqvm crate.

Basic Usage

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
b.emit_push(10)
 .emit_push(32)
 .emit_add()
 .emit_halt();

let program = b.build().unwrap();
assert_eq!(program.code().len(), 6);
}

Labels

Labels are opaque handles. Create them with label(), anchor them with place(), and reference them in emit_jump()/emit_jump_if(). Both forward and backward references work. place() returns a Result – placing the same label twice, or placing a label from a different builder, is an error.

Backward Reference

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let loop_top = b.label();

b.emit_push(3);
b.place(loop_top).unwrap();  // anchor label at this position
b.emit_push(-1);
b.emit_add();
b.emit_copy();
b.emit_jump_if(loop_top);    // backward jump to loop_top
b.emit_pop();
b.emit_halt();

let program = b.build().unwrap();
}

Forward Reference

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let done = b.label();

b.emit_push(0);
b.emit_jump_if(done);         // forward jump -- target not yet placed
b.emit_push(42);
b.place(done).unwrap();       // anchor here
b.emit_halt();

let program = b.build().unwrap();
}

PUSH Auto-Sizing

emit_push(val) automatically selects the smallest PUSH1PUSH8 instruction:

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
b.emit_push(0);         // emits PUSH1 (2 bytes)
b.emit_push(42);        // emits PUSH1 (2 bytes)
b.emit_push(1000);      // emits PUSH2 (3 bytes)
b.emit_push(i64::MAX);  // emits PUSH8 (9 bytes)
b.emit_halt();

let program = b.build().unwrap();
assert_eq!(program.code().len(), 17);  // 2 + 2 + 3 + 9 + 1 (HALT)
}

Register Operations

Most register instructions have a corresponding method:

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Register};

let mut b = InstructionBuilder::new();
b.emit_push(42)
 .emit_stow(Register(0))     // r0 <- Int(42)
 .emit_load(Register(0))     // push r0's value back onto the stack
 .emit_bqmx(Register(1))     // pop 42 as size; allocate a binary model in r1
 .emit_halt();

let program = b.build().unwrap();
}

DROP is available as emit_drop(). It resets the register to Unset, the same state an unwritten register starts in – not Int(0):

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Register};

let mut b = InstructionBuilder::new();
b.emit_drop(Register(5));  // r5 <- Unset
b.emit_halt();

let program = b.build().unwrap();
}

ENERGY

The emit_energy() method takes two register operands:

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Register};

let mut b = InstructionBuilder::new();
b.emit_energy(Register(0), Register(1));  // ENERGY r0 r1
b.emit_halt();

let program = b.build().unwrap();
}

Raw Instruction Emit

For instructions without a dedicated method, use emit():

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Instruction};

let mut b = InstructionBuilder::new();
b.emit(Instruction::Copy {})
 .emit(Instruction::Halt {});

let program = b.build().unwrap();
assert_eq!(program.code().len(), 2);
}

Build Errors

build() validates all labels and returns errors for:

  • UnplacedLabel – a label was used in a JUMP/JUMPI but never placed.
  • UnusedLabel – a label was placed but never referenced by any jump. The label at byte offset 0 is exempt from this check: the builder treats offset 0 as the implicit entry point, so a label placed there and never jumped to does not count as unused. (The l0/l1 example in the next section relies on exactly this – l0 sits at offset 0 and is never referenced by a jump, yet build() succeeds.)
  • TooManyTargets – more than u16::MAX + 1 (65,536) labels were placed in the program, exceeding the wire-format limit on sequential TARGET ids.
  • FixupOutOfBounds – a fixup site falls outside the assembled buffer. This indicates a bug in the builder itself, not a mistake in caller code.

Two more Error variants exist but are returned by place(), not build(): DuplicateLabel (a label placed more than once) and ForeignLabel (a label passed to a builder that did not create it).

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let ghost = b.label();
b.emit_jump(ghost).emit_halt();
assert!(b.build().is_err());  // UnplacedLabel
}

Labels and the JumpTable

Every place() call anchors a label to the current write position and emits an inline TARGET opcode there. build() resolves each JUMP/JUMPI fixup against the placed TARGET positions, renumbers labels into stream order, and returns a Program. The result of that resolution is queryable afterward: Program::jump_table() returns a JumpTable, mapping each TARGET’s sequential id (0, 1, 2, ... in program order) to its byte offset.

JumpTable::len() equals the number of distinct labels placed, and JumpTable::get(id) returns the byte offset for a sequential id:

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let l0 = b.label();
let l1 = b.label();
b.place(l0).unwrap()
 .emit_nop()
 .emit_jump(l1)
 .place(l1).unwrap()
 .emit_halt();

let program = b.build().unwrap();
assert_eq!(program.jump_table().len(), 2);
assert_eq!(program.jump_table().get(0), Some(0));  // first TARGET, byte 0
assert_eq!(program.jump_table().get(1), Some(4));  // second TARGET, byte 4
}

This mapping is built once, at load time, by a single scan of the instruction stream for TARGET opcodes – it is not part of the .xqb wire format. See Execution for how the VM’s run loop resolves a Jump result against it.

Pallet Fixture

pallet-xqvm embeds xqvm inside a Substrate FRAME runtime, as a compile-time and runtime integration gate. Its own module documentation states the purpose plainly: if xqvm’s public API changes in a way that breaks Substrate pallet integration, CI fails here first. fixtures/pallet-xqvm is an excluded member of the main Cargo workspace, declared in Cargo.toml, because it pulls in a heavy polkadot-sdk git dependency. Treat this chapter as a reference integration and a fixture to build on, not as a supported deployment path with its own release cycle. Which opcodes a runtime that embeds the VM may admit, and what it has to enforce to admit them, is stated separately in On-Chain Admissibility.

CI still runs it, and blocks on it. test:substrate in .gitlab/ci/test.yml is path-gated: on a merge request or a feature branch it runs only when the diff touches xqvm, the fixture, or the files that define the job itself, and it runs unconditionally on protected refs and release tags. It carries no allow_failure:, so it blocks the pipeline whenever it is created. make test-substrate-fixture runs cargo test --manifest-path fixtures/pallet-xqvm/Cargo.toml, exercising eight pallet tests plus two runtime-integrity checks FRAME generates from construct_runtime!. Every claim below is checked against fixtures/pallet-xqvm/src/lib.rs.

Configuration

The pallet is configured via the Config trait:

#[pallet::config]
pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
    /// Maximum byte length of an uploaded XQBC program.
    #[pallet::constant]
    type MaxProgramSize: Get<u32>;

    /// Maximum number of calldata input slots and output slots.
    #[pallet::constant]
    type MaxCalldata: Get<u32>;
}

Two associated types, both bounds rather than tunable behaviour. The mock runtime used by the fixture’s own tests sets MaxProgramSize = 65_536 (64 KiB) and MaxCalldata = 256, matching the VM’s 256-slot register file. A single MaxCalldata bound caps both the calldata a caller supplies and the outputs the pallet can return – there is no separate output-side limit.

Storage

#[pallet::storage]
pub type StoredProgram<T: Config> =
    StorageValue<_, BoundedVec<u8, T::MaxProgramSize>, OptionQuery>;

One value, not a map. StoredProgram holds the bytecode of the most recently executed program; calling the pallet’s extrinsic again overwrites it. There is no per-program storage keyed by hash, and no record of which account submitted which program beyond the event stream below.

The submit_program Extrinsic

The pallet exposes exactly one dispatchable, call index 0:

#[pallet::call_index(0)]
pub fn submit_program(
    origin: OriginFor<T>,
    bytecode: BoundedVec<u8, T::MaxProgramSize>,
    calldata: BoundedVec<i64, T::MaxCalldata>,
    output_slots: u32,
    step_limit: u64,
    memory_limit: u64,
) -> DispatchResult
ParameterTypeDescription
bytecodeBoundedVec<u8, T::MaxProgramSize>XQBC-encoded program, produced by InstructionBuilder::build().encode() or by assembling .xqasm source.
calldataBoundedVec<i64, T::MaxCalldata>Integer values injected as RegVal::Int into the VM’s calldata slots, in order.
output_slotsu32Output slots to reserve. The caller declares its own arity; see step 3. May not exceed MaxCalldata.
step_limitu64Instructions the run may execute. The bound is exact: 0 executes nothing and the call fails with ExecutionFailed rather than succeeding vacuously.
memory_limitu64Bytes the run may allocate. Charged before each allocation, so an over-large one returns ExecutionFailed instead of reaching the allocator.

Decode, execute, store, in that order:

  1. Require a signed origin (ensure_signed); an unsigned call is rejected with BadOrigin before pallet logic runs.

  2. Decode bytecode as an xqvm::Program. A decode failure returns Error::BytecodeInvalid and nothing is stored.

  3. Take the output-slot count from the caller’s output_slots argument, rejecting a request above MaxCalldata with OutputSlotsTooLarge.

    Do not derive it from program.output_slots(). That header byte is a saturating count of OUTPUT instructions, not of slots, so one OUTPUT inside a loop that writes slots 0 and 1 records 1; sizing the output vec from it makes the slot-1 write raise OutputIndex and the call fail with ExecutionFailed, rejecting a program the VM and the verifier both accept. See the bytecode format, which states that neither header count may be used to pre-size a slot array.

  4. Build a fresh Vm, set the calldata, the output-slot count and the caller’s two budgets, and run the program. Any VM fault returns Error::ExecutionFailed; the pallet does not distinguish which fault occurred, so a budget exhausted one instruction short of HALT is indistinguishable from a decode-clean program that divided by zero.

  5. Collect the Int outputs (any other RegVal variant in an output slot is silently dropped from the result) and bound them to T::MaxCalldata. Exceeding that bound returns Error::OutputOverflow.

  6. Store bytecode in StoredProgram, overwriting any previous value, and emit Event::ProgramExecuted { who, outputs }.

There is no separate store-then-execute split and no program lookup by hash. Both budgets are extrinsic arguments rather than pallet constants because that is the shape the real pallet has to take: what a caller pre-pays for is what the VM may spend, and a budget the caller cannot name is a threat model the fixture cannot express.

The allocation budget is the one that has to be named on-chain rather than inherited. Vm::new() installs 1 GiB, which is far more than a wasm32 runtime heap, so the heap gives out long before the budget does and the allocator traps the whole execution instead of returning a fault the pallet can report as ExecutionFailed. A caller-supplied memory_limit, capped by MaxMemoryLimit, is what keeps an over-large allocation a reportable dispatch error. The mock sets the cap to the VM’s own 1 GiB so that only the caller’s ability to name something smaller is under test; a production runtime sets it to a figure its heap can actually honour.

What the fixture does not do is call xqvm::verifier::verify: it decodes the bytecode and runs it, so every static check the verifier performs is skipped, and the faults those checks would have caught surface at runtime as ExecutionFailed instead. A production pallet must verify before it executes, for the reasons set out in On-Chain Admissibility.

Off-chain, produce the bytecode however you like; the assembler CLI is xquad asm.

The cargo profile the runtime is built with is a second default an operator must not inherit without reading it. Do not compile the runtime that carries this pallet with overflow-checks = true until every #[expect(clippy::arithmetic_side_effects, ...)] entry in xqvm has been re-read as a claim about that build: the VM already raises ArithmeticOverflow for the arithmetic it performs on a program’s behalf, so the flag adds nothing there and instead turns the host-side operations that allow-list records into panics, and a panic inside block execution is a block-production fault, strictly worse than a wrong answer. The flag would not touch the VM’s deliberate wraps, which are method calls (SHL’s wrapping_shl, SLACK’s wrapping_mul); it acts on the bare operator sites, which are exactly the ones the allow-list claims cannot leave range. That is why the reading is the gate, and it does not expire once the allow-list exists.

Weight

#[pallet::weight(Weight::from_parts(10_000, 0).saturating_add(T::DbWeight::get().writes(1)))]

A fixed placeholder, not a metered cost model: the same weight is charged regardless of program size or step count actually used. The extrinsic neither computes an actual weight from vm.steps() nor refunds any difference via PostDispatchInfo. The pallet’s own source comment says as much: a production integration must supply real benchmarks. This weight exists only so the extrinsic compiles and the fixture’s tests can dispatch it.

Events and Errors

EventFieldsDescription
ProgramExecutedwho, outputsEmitted once, after a successful run. outputs holds only the Int-valued output slots, in slot order.
ErrorMeaning
BytecodeInvalidbytecode failed XQBC decode.
ExecutionFailedThe VM faulted at runtime – stack underflow, an unresolved jump, or any other xqvm::Error variant, all mapped to this one case.
OutputOverflowThe program produced more Int outputs than MaxCalldata allows.
StepLimitTooLargestep_limit exceeds MaxStepLimit. Checked before the decode, so an over-budget request never pays to parse the program it would have run.
MemoryLimitTooLargememory_limit exceeds MaxMemoryLimit. Checked before the decode, for the same reason.
OutputSlotsTooLargeoutput_slots exceeds MaxCalldata.

What the Fixture’s Tests Check

fixtures/pallet-xqvm/src/tests.rs covers five groups, sixteen tests in all:

  • Happy paths – an arithmetic program with no calldata, a calldata passthrough, and a two-value sum.
  • Error paths – invalid bytecode, a stack underflow that reaches ExecutionFailed, and an unsigned origin rejected by ensure_signed.
  • The step budget – a request above MaxStepLimit, one exactly at it, a zero budget, and a budget shorter than the program.
  • The allocation budget – a request above MaxMemoryLimit, one exactly at it, and a budget a BQMX outgrows. The third is the one the argument exists for: it fails outright if submit_program stops calling set_memory_limit, which is how the gap this closes went unnoticed.
  • The output-slot count – one OUTPUT inside a loop writing a slot per iteration, the same program with the count the header byte would have supplied, and a count above MaxCalldata. The middle test asserts program.output_slots() == 1 directly, so the pair records what that header byte is and is not good for.

Running make test-substrate-fixture against this tree passes eighteen: the sixteen above plus two FRAME-generated checks, a genesis-config build and a construct_runtime! integrity test.

Calldata Limitations

submit_program only accepts i64 calldata (not models, vectors, or samples), because BoundedVec<i64, T::MaxCalldata> is the extrinsic’s parameter type. A program that needs a richer input must construct it internally rather than receive it from the caller.

On-Chain Admissibility

A runtime that embeds xqvm runs bytecode it did not write, submitted by an account it does not trust, inside a block whose weight it has to declare in advance. The question this page answers is which of the VM’s 93 opcodes such a runtime may admit.

The answer is all of them, and not as a concession each runtime makes separately. On-chain verifiable deterministic execution is what XQVM is for, so chain compatibility is a property of the instruction set rather than a per-embedder admission decision. An operation that cannot be made to meet the bar below is not an opcode that needs gating: it is functionality that belongs in xqcp, xqsa, the xquad API or a helper library, and it never enters the ISA at all.

This page is the reader-facing summary of that rule. All six clauses are specified where a third implementation reads them: clause 1 in spec/xqvm/SPEC.md’s Type System, which makes checked arithmetic normative with no implementation-defined alternative; clause 2 in the same file’s Determinism section, which states that an instruction’s result is a function of the program, the calldata and the VM’s own state, and that no instruction reads host state the calldata did not carry; clause 3 in HLF.md’s ordering rules, covering delta application, sparse-table accumulation and the grid fold; clause 4 in SPEC.md’s Allocation budget section, which charges every allocation before it happens; clause 5 in METERING.md, which fixes the step schedule, its charging points and its counting units; and clause 6 in SPEC.md’s Faults section, which gives every fault a normative identity that ISA.md is bound to name faults by, together with the conformance vectors that pin the behaviour itself. One row of that section records an identity that is not yet settled; the section on what is not enforced says which.

The gate that holds a proposed opcode against all six belongs to the contribution process rather than to the specification. It is stated in docs/guide/development-workflow.md under “The opcode-addition gate”: adding a row to the opcode table requires the merge request to argue each of the six clauses in its description, and an opcode that cannot clear all six does not ship. There is deliberately no CI guard for it, because the gate asks for a correctness argument a reviewer weighs rather than a string a script can find.

The bar

An XQVM opcode has to satisfy all of the following.

  1. No floating point. Integer arithmetic only, with every operation range-checked so that it faults rather than wrapping.
  2. No host I/O, wall-clock, or ambient state. An instruction’s result is a function of the program, the calldata and the VM’s own state.
  3. No nondeterministic iteration order. Anything that walks a collection walks it in an order the spec fixes.
  4. Bounded allocation. Every allocation is charged against a budget before it happens, and the budget is not escapable by a value the submitting account controls.
  5. Bounded per-instruction work. The work one instruction performs is charged against the step budget before it happens, at a rate that scales with the data the program controls, so that a step is a unit of cost rather than a unit of dispatch.
  6. Specified behaviour, pinned by a vector. The result and every fault the opcode can raise are specified normatively in spec/xqvm/, and a conformance vector covers the behaviour, failure paths included.

Rules 4 and 5 are the two halves of bounded work, and rule 5 is the one that is easy to lose: an allocation budget bounds how much an instruction keeps, not how long it takes. Rule 6 is the one that is easy to skip. A chain does not need determinism against a single implementation; it needs behaviour that a reader of the specification can predict, because a divergence between the specification and an implementation is unspecified behaviour with a plausible result, and no memory budget makes that safe. Today that check is implemented as agreement between the Rust VM and the Python reference interpreter, checked by the conformance suite; after QUI-1082 removes the Python interpreter, spec/xqvm/ and the vectors derived from it remain the whole of rule 6.

Because the bar is a property of the instruction set, the denied set is empty by construction. A proposed opcode that cannot clear it does not ship, so no embedder inherits a per-opcode decision.

Why not an allowlist

An earlier framing of this question asked which opcode families to admit and which to gate behind a later runtime upgrade, on the theory that the integer core is cheap and the model-building surface is not. That framing is no longer the right one: the model-building opcodes are exactly the reason to run XQVM on chain at all, and the two properties that made them unsafe – unbounded allocation and Rust-versus-Python divergence – were defects with owners rather than inherent properties of the family.

So a runtime should not carry a static allowlist scanned over the decoded instruction stream. With the bar above there is no denied set for such a scan to find, and an allowlist would be code that has to be revisited on every addition to the instruction set, silently rejecting valid programs until someone remembers to update it. The static half of enforcement is the verifier, which checks properties of the program rather than membership of its opcodes.

Where the bar is enforced

LayerRunsEnforces
Staticxqvm::verifier::verify at program submissionStructural integrity, jump targets, loop balance, register type-state, must-init, stack depth
RuntimeVm::set_memory_limit before executionThe allocation budget, charged before each allocation
RuntimeVm::set_step_limit before executionThe step budget, charged before each unit of work
WeightThe extrinsic’s declared and refunded weightThe block’s share of the above

The static layer is what makes a rejection cheap: a program that cannot run correctly is refused before it consumes execution weight. The runtime layer is what makes admission safe: the verifier does not bound allocation or running time, and cannot, because both depend on values computed at runtime. Neither layer substitutes for the other.

The budgets belong in the runtime’s configuration rather than in the VM defaults. The library defaults documented in Limits and Errors – 10,000,000 steps and a 1 GiB allocation budget – are sized for an off-chain host, and a 1 GiB budget admits a sample of 134 million variables. A chain has to set both from its own Config, sized so that the worst case a submitted program can reach still fits the block.

Family by family

The table below uses the same fourteen categories as the Instruction Set Reference, so the two can be read side by side. Cost is what an instruction charges against the allocation budget; the full schedule, with rates, is in Limits and Errors. Several categories charge different rates for different opcodes, and the cells say which. The step budget is a separate schedule with its own rates, specified in spec/xqvm/METERING.md; an opcode free against one budget is not necessarily free against the other. ENERGY and the four grid readers are the clearest cases: each allocates nothing and each charges the step budget per unit of work it does.

FamilyCountCostNotes
Control flow12ITER charges for the slice it copies – 8 bytes per element of a vec<int>, a whole-model copy per element of a vec<xqmx>; the rest are freeLoop balance is checked statically; RANGE and ITER skip an empty body rather than running it once. The loop frame itself is uncharged but capped: the loop stack holds 8,192 frames and a program that grows past that faults with LoopStackOverflow. LVAL copies the current loop value without charging the allocation budget – see below. NOP and HALT live here
Register I/O5INPUT and OUTPUT charge for the value they copy across the host boundary, at the schedule’s own rates; the rest are freeBoth are also bounded by the calldata and output-slot counts the runtime sets. A model copies at the whole-model rate ITER pays, but a vec<int> costs 16 bytes per element here against ITER’s 8: the copy rate prices a Vec that grew by doubling, which is what VECPUSH charged for, while ITER allocates its slice at exact capacity
Stack manipulation12FreeDepth is bounded at 8,192 items, checked statically and at runtime
Arithmetic13FreeInteger only; every operation is range-checked and faults rather than wrapping
Comparison5Free
Logical4Free
Bitwise6FreeShift amounts outside [0, 64) fault
Allocators9BQMX, SQMX, XQMX: 8 bytes per declared variable. BSMX, SSMX, XSMX: 8 bytes per variable. VEC, VECI, VECX: freeThe three charges are not the same kind. A model stores coefficients sparsely, so its charge is a proxy for what every consumer of the model has to materialise; a sample allocates a dense buffer at that rate, so its charge buys real bytes. An empty vec allocates nothing and its storage is charged as it grows
Vector operations5VECPUSH: 16 bytes per element. SLACK: 16 bytes per element appended, two per entry. VECGET, VECSET, VECLEN: freeSLACK appends to two vecs and is charged for both, so an entry costs 32 bytes; the three access forms neither grow nor allocate
Index math2FreePure index arithmetic on the stack
Coefficient access6SETLINE, ADDLINE: 32 bytes per linear coefficient. SETQUAD, ADDQUAD: 48 bytes per quadraticReads are free, and so are writes into a sample, whose buffer was charged when it was allocated. Both rates are literals rather than derived from the host’s pointer width, so a wasm32 runtime charges what the reference VM and the book state
Grid operations5FreeA grid reinterprets variables the program already declared and paid for: RESIZE rejects extents whose product exceeds the model’s size, which bounds memory. The step budget bounds the work: ROWSUM, COLSUM, ROWFIND and COLFIND charge GRID_CELL_STEPS per cell of the axis they scan, after validating the operand and before walking it, whether or not the scan short-circuits
High-level constraints8EXCLUDE: one quadratic term. IMPLIES: one linear and one quadratic. REDUCE: one variable, three quadratic terms and one linear. ONEHOTR, ONEHOTC, EQUALITY, ATLEAST, ATLEASTW: worst-case expansion, one linear term per variable and one quadratic term per pairOnly the last five expand; see below
Energy1FreeAllocates nothing, and charges the step budget for the sample it copies and every model term it accumulates

Ninety-three opcodes across the fourteen categories, all of them integer-only by construction: the ISA has no floating-point type, no floating-point opcode and no way to produce one. What the conformance vectors pin is the stronger property that makes this useful for consensus – the checked-arithmetic rule, under which Rust’s i64 and Python’s unbounded integers agree on every result and fault the same way on every overflow.

The expanding constraints deserve a second look

Three sets are worth separating, because Limits and Errors separates them and a weight model needs each:

  • Quadratic in a stack-controlled operand. ONEHOTR, ONEHOTC, EQUALITY, ATLEAST and ATLEASTW write a number of coefficients that grows with the square of a term count the caller chooses.
  • Allocating variables. EQUALITY, ATLEAST, ATLEASTW and REDUCE add variables to the model. REDUCE adds exactly one.
  • Both at once, from a single operand. EQUALITY, ATLEAST and ATLEASTW.

EXCLUDE, IMPLIES and REDUCE charge constant amounts and do not expand. Every opcode in the first set is charged for its worst case before it writes anything, so the aggregate budget does bound them. A runtime that wants a submitted program to fail early and cheaply rather than after spending most of its budget in one instruction should consider a per-instruction cap on top of the aggregate one. That is a pricing decision, not an admissibility one.

What is not enforced today

The bar above is what the instruction set is designed to, and most of it is now backed by code. The v0.4.0 hardening pass bounded grid extents to the allocation a program already paid for, made the coefficient rates target-independent literals, charged the INPUT/OUTPUT copy across the host boundary, capped the loop stack, and closed the three Rust-versus-Python divergences in the model-building surface. Step metering then made a step a unit of cost rather than a unit of dispatch: every instruction charges a base cost before dispatch, the opcodes whose work scales with caller-controlled data charge for that work before doing it, and the forward scan of an empty-loop skip charges for each instruction it consumes. The four grid scans charge GRID_CELL_STEPS per cell of the axis they walk, which was the last place a memory bound stood in for a work bound: RESIZE bounds the extent by an allocation the program paid for, and that bounds what the grid holds, not what a scan over it costs. An embedder pricing WeightPerStep * steps is pricing work rather than dispatches.

The same pass closed the sample value domain, which had been documented as a producer convention rather than a rule: SETLINE and ADDLINE now raise SampleOutOfDomain for a write outside the domain the register’s allocator declared. It is a bounded per-instruction check on a value already on the stack, so it costs nothing against the bar above, and it removes a way for a program to hand a solver an assignment that is not an assignment.

What is left is narrow.

A tracer copies registers outside the step budget. The Rust VM clones every register an instruction reads and writes around each dispatch when a tracer is attached – O(model) per instruction, charged nothing, because the observable step count must not depend on whether a tracer is attached. This is a constraint on the embedder rather than a hole in the meter: a chain runs NoopTracer and the branch compiles out. A runtime that attaches a real tracer meters untrusted programs untraced, or accounts for the tracing copy outside the step budget. Stated normatively in spec/xqvm/METERING.md under Conformance.

LVAL copies without charging the allocation budget. The step budget now prices the copy – LVAL charges for the coefficients it clones out of a loop frame – but the bytes that copy occupies are not charged, so a vec<xqmx> iterated with LVAL in the body holds one unaccounted model copy per live register beyond what ITER paid for. Bounded by the 256-slot register file, so a constant factor rather than an unbounded obligation, but one a memory bound has to include.

The reference integration does not verify. The fixture takes both budgets from the caller and bounds them with MaxStepLimit and MaxMemoryLimit, which is the shape a real pallet needs, so the runtime layer of the boundary is now exercised end to end. The static layer is not: it never calls xqvm::verifier::verify – it decodes and runs, so every static check is skipped and the faults those checks would have caught surface at runtime as ExecutionFailed. Running the verifier on chain is QUI-1055; pricing the budgets into a benchmarked weight rather than the fixed placeholder is QUI-1012.

Cross-implementation agreement, meanwhile, is close to where the bar wants it. make opcode-parity holds the two opcode tables to each other, make conformance runs every vector on both interpreters, and the metering constants are mirrored value for value in xqvm_py/metering.py under their own parity check. The vectors now cover the model-building surface that the arithmetic-only suite once missed, including failure paths: constraints without a grid, invalid grid dimensions, integer allocation, index-math operand ordering, and accumulation overflow in energy and grid sums.

Operand validation order is part of that agreement and is now normative: spec/xqvm/METERING.md fixes it under Conformance, and both VMs validate each register completely before looking at the next. What the specification has not yet settled is the identity of a mode fault. xqvm::Error has no mode variant – a RegVal is either a model or a sample, so the Rust VM reports a sample in a model slot as a register-type error, where xqvm_py, whose XQMX carries a mode flag, reports a mode error. That difference spans every mode check rather than one opcode, predates step metering and is tracked separately. Unsettled is not the same as permitted: SPEC.md’s Faults section holds every identity normative and records this row as the one still unresolved, so one of the two implementations is wrong and a third must not read either spelling as settled. A runtime that surfaces fault identity to submitters should know it is not yet uniform. See Conformance for how the suite is structured and what adding a vector involves.

Conformance

XQuad ships two independent VM implementations: the Rust xqvm crate this book otherwise documents, and a pure-Python reference VM, xqvm_py. A program compiled once to XQVM bytecode is meant to produce the same result on either. The conformance harness – xquad-conformance – is the mechanical check of that claim. This page explains what its result means for you: whether you are embedding xqvm, writing against xqvm_py, or deciding how much to trust either one.

What a Vector Is

A conformance vector is a directory holding a fixed test case: a canonical .xqasm program, the calldata it runs with, and the outputs and residual stack it is expected to produce. The harness assembles the program, runs it on both VMs, and asserts each one’s observed result matches the recorded expectation.

Each vector directory holds three files:

  • program.xqasm – the canonical, human-readable source. The assembler is authoritative: whatever xqasm::assemble_source() produces from this file is the canonical bytecode, assembled in-process on every run. No pre-assembled bytecode artifact is committed.
  • inputs.json – calldata, output slot count and the two budgets the run is given, for example {"calldata": [6, 7], "output_slots": 16}. Every key but calldata is optional: output_slots defaults to 16, step_limit to 10000000 and memory_limit to 1073741824, each matching xquad run. The harness resolves the defaults itself and passes them to both VMs, so a budget is a property of the vector rather than of whichever default each runner carries. Set one only for a vector that is about that budget.
  • expected.json – the recorded result, for example {"outputs": [42], "final_stack": []}. outputs is a sparse map: each entry is an i64 written by OUTPUT, or null for a slot that was reserved but never written; trailing unset slots are omitted entirely, so a program that writes slot 0 of 16 reserved slots produces [42], not [42, null, ...]. Explicitly-written zeroes are preserved – only slots OUTPUT never touched disappear. final_stack is the residual stack at HALT, bottom to top.

Vectors are grouped, for human navigation, into eight directories under conformance/vectors/: arithmetic, constraints, control-flow, energy, index-math, metering, vector-ops, and xqmx-grid. These names are not the same vocabulary as opcodes.yaml’s own category field, which has 15 values and matches spec/xqvm/SPEC.md’s section names, not the vector directories. Only five names happen to coincide (arithmetic, control-flow, index-math, vector-ops, xqmx-grid); constraints, energy and metering have no matching YAML category at all – the constraint opcodes are catalogued under xqmx-high-level, and so is ENERGY itself, while a metering vector is about a budget rather than about any one opcode. Don’t expect a vector directory to line up with an opcodes.yaml category by name.

Running the Harness Locally

# Full matrix: both runtimes, every vector
cargo test -p xquad-conformance

# Rust only
cargo test -p xquad-conformance --no-default-features --features rust

# Python only (needs uv and a synced workspace venv)
cargo test -p xquad-conformance --no-default-features --features python

# Triage a single vector on both runtimes
cargo run -p xquad-conformance -- --filter arithmetic/add_basic --impl both

The Python runs shell out through uv run python -m xqvm_py, so the workspace venv is picked up without a manual activation step. XQUAD_CONFORMANCE_PYTHON overrides the uv wrapper command, not the interpreter behind it.

What Passing Means

Every vector runs as two separate tests, one per runtime, generated at build time so a regression in one implementation cannot be masked by the other passing. CI runs those two test suites within the verify:parity job’s make -k check-parity, which keeps running every target after one fails so a Rust-side failure cannot suppress a Python-side one (or vice versa). A vector that passes on both VMs means: for that specific program and that specific calldata, both implementations agree on every output value and the final stack. It says nothing about programs the vector does not cover.

Two more guarantees apply alongside vector agreement, each checked separately:

  • The opcode table itself – a build-time assertion ties the Rust opcodes! macro to opcodes.yaml, and a CI script ties opcodes.yaml to xqvm_py’s own opcode table, so the two implementations cannot silently diverge on which opcodes exist or how many operands they take.
  • Bytecode encoding – owned by the xqasm crate’s own test suite, not by this harness. Conformance vectors trust the assembler to produce correct bytecode from .xqasm source; they do not re-check the encoding independently.

For the architectural reason two implementations exist at all – independent verification of a solver’s answer, not just parity between runtimes – see Three Programs. For what each VM actually does with a program, see the XQVM Reference.

Coverage Is Per-Opcode, and Incomplete

A vector exists only where someone wrote one, and conformance/vectors/ holds far more vectors than it covers distinct opcodes: bitlen_small and bitlen_negative both cover BITLEN; three separate slack_* vectors all cover SLACK; the xqmx-grid directory alone spends seventeen vectors on six opcodes. Where a vector is missing, the harness makes no claim about that opcode at all. Passing CI does not mean every opcode has been checked for cross-implementation agreement – only that every opcode a vector currently exercises has been.

Which opcodes those are is now computed rather than guessed. CI prints the report on every pipeline, as the last step of make check-parity, and it runs locally with:

make conformance-coverage

The report gives two numbers, because one is not enough. Present counts opcodes appearing in some vector’s assembled program; reached counts those a vector executes to completion. Reached is the stronger measure and always the smaller one: an opcode behind an untaken branch is present but not reached, and so is the instruction an error vector exists to make fault – a faulting instruction never completes a step. Reporting only “reached” would call DIV uncovered despite arithmetic/div_by_zero; reporting only “present” would credit an opcode sitting in dead code.

Both numbers sit below the size of the opcode table, and the report names the shortfall rather than only counting it. Run make conformance-coverage for the current figures; this page does not repeat them, because they move whenever a vector lands. The opcodes in no vector at all are the real holes. Those present but never reached are a weaker signal worth knowing: each has an error vector pinning how it fails, and no vector pinning what it does when it succeeds.

Coverage reports; it does not gate on completeness, which would fail today. It does gate on regression – conformance/tests/coverage.rs holds both numbers as floors that a merge request may raise and may not lower without saying why.

IDXTRIU is the worked example of what that costs. It had no vector, and the two implementations disagreed on it in two separate ways: on operand order, and on whether an intermediate that leaves i64 range faults. Both were found by reading the implementations side by side rather than by any mechanical check, and both are now closed, with index-math/idxtriu_intermediate_overflow and index-math/idxgrid_intermediate_overflow pinning the second. Neither gap was exotic; both were simply in the part of the opcode table nothing had written a vector for – which is the hole the coverage report exists to make visible before someone has to find it by reading.

Treat a green conformance run as evidence for the programs it actually tests, not as a blanket guarantee that the two implementations agree on every opcode in every configuration.

Glossary

Terms as this book uses them, alphabetically. Each entry links to the page that treats it in full.

Backend. Where a model actually solves: locally on a CPU or GPU, on D-Wave’s cloud QPU, or on the Quip network. Choosing one does not change the model, the encoder, or the verifier. See Backends.

Calldata. The read-only array of values a host program supplies to a VM run before execution starts. INPUT reads from it by slot index. See Calldata and Outputs.

Chain strength. A D-Wave QPU parameter. It sets how strongly a group of physical qubits standing in for one logical variable are coupled together, so they read out as one value rather than breaking apart under the problem’s own couplings. See D-Wave QPU.

Conformance vector. A fixed test case – program, calldata, and expected output – that the conformance harness runs on both the Rust and Python VMs and checks for agreement. See Conformance.

Decoder. One of the three programs a problem compiles to: takes a sample and extracts the answer in the problem’s own terms (a tour, a partition, a set of selected items). See Three Programs.

Domain. The set of values a model’s variables take: binary (\({0, 1}\)), spin (\({-1, 1}\)), or integer (\({0, \ldots, k-1}\)). A model and the sample solving it must share a domain. See Quadratic Models.

Embedding (D-Wave). Mapping a model’s logical variables onto a QPU’s physical qubit graph, called minor embedding: each logical variable becomes a chain of one or more physical qubits held together by chain strength (above). SolverDWaveQPU builds this automatically. See D-Wave QPU.

Embedding (Rust). Using the xqvm and xqasm crates directly from a Rust program, without the Python packages. See Embedding Overview.

Encoder. One of the three programs a problem compiles to: reads runtime inputs from calldata and builds the model. See Three Programs.

Energy. The single number a model computes for a candidate assignment – the Hamiltonian evaluated at that point. A solver searches for the assignment that minimises it; ENERGY computes it directly so a program can check a solver’s answer instead of trusting it. See Quadratic Models.

Hamiltonian. The function a quadratic model represents: a sum of linear and quadratic terms over the variables, borrowed from the physics term for a system’s total energy. See Quadratic Models.

Ising model. The spin domain: variables take values \(-1\) or \(+1\), the domain quantum annealing hardware minimises natively. See Quadratic Models.

Model. An XqmxModel: two sparse coefficient maps, linear and quadratic, that together define a Hamiltonian over a fixed number of variables. Built with BQMX/SQMX/XQMX and the coefficient-access opcodes. See Quadratic Models and VM Architecture.

Output slot. The writable array a VM run populates via OUTPUT, read back by the host program after the run halts. See Calldata and Outputs.

Penalty. A term added to a model’s Hamiltonian that is zero when a constraint holds and positive when it does not. Minimising the combined Hamiltonian then tends to satisfy the constraint too. Sizing the penalty weight correctly is the real engineering problem in constraint-by-penalty modelling. See Quadratic Models and Constraints.

QUBO. Quadratic Unconstrained Binary Optimisation: the binary domain, where each variable is either selected or not. See Quadratic Models.

Register. One of 256 typed slots (r0-r255) holding a RegVal: an integer, a vector, a model, or a sample. See VM Architecture.

Sample. An XqmxSample: one candidate assignment, a single value per variable, produced by a solver or built by hand. Shares a domain and variable count with the model it answers, but not its shape. See Quadratic Models.

Solver. Anything implementing the xqsa interface: takes a model, returns a sample and its reported energy. xqsa ships five: a CPU annealer, CUDA and Metal GPU annealers, the D-Wave QPU, and the Quip network. See Solving Overview.

Step limit. The maximum number of instructions a VM run will execute before faulting with StepLimitExceeded, guarding against runaway programs. Configurable per run; defaults to 10,000,000. See Limits and Errors.

TARGET. The opcode marking a valid jump destination. Every label a program jumps to must have a TARGET at that position. The VM scans for them once at load time, rather than reading a precomputed mapping out of the wire format, and resolves jumps against that scan. See Execution and Builder API.

Verifier. Two related but distinct things in this book. The bytecode verifier is static analysis (xqvm::verifier) that checks a program’s structure before it runs: every jump lands on a TARGET, and every register is read only after it is written. See Verifier. The verifier program is one of the three programs a problem compiles to: it checks a sample against the encoder’s constraints and recomputes its energy independently. See Three Programs.

XQBC. The binary wire format: a 15-byte header (magic, version, slot counts, code length, checksum) followed by the raw instruction stream. See Bytecode Format.

XQCP. X-Quadratic Constraint Programming: the Python DSL that turns a problem description into the three XQVM programs. See Modelling Lifecycle.

XQMX. The matrix type the VM allocates and manipulates, in either of two modes: a model (XqmxModel, a Hamiltonian’s coefficients) or a sample (XqmxSample, one candidate assignment). See VM Architecture.

XQSA. X-Quadratic Solver Adapters: the Python package holding one adapter per solving backend. See Solving Overview.

XQVM. X-Quadratic Virtual Machine: the stack machine every XQuad problem compiles to, and the layer both the Rust and Python implementations implement. See XQVM Reference.

Stability

XQuad is pre-1.0. The root README.md says so directly: the instruction set, the binary format, and the public API may still change before v1.0, and production use is not recommended yet. Read everything below in that light – it describes the project’s current process discipline, not a promise that anything is frozen.

What Is Versioned Together

Eight packages ship from this repository, three to crates.io (xqvm, xqasm, xqcli) and five to PyPI (xqffi, xqvm_py, xqcp, xqsa, xquad). All eight always carry the same version. They release together: one release MR bumps every version, one tag triggers the one pipeline that publishes all eight, and the changelog is generated from that same tag. There is no independent release cadence per package today.

What CI Actually Guards

Two things fail the build if they drift, checked mechanically by default rather than caught only by review discipline; the second of the two has a deliberate, contributor-controlled way out, noted below:

  • Rust and Python VM agreement. Conformance checks that xqvm and xqvm_py agree on every behaviour a vector covers. Coverage is real but partial – see that page for what “partial” means concretely.
  • Spec and implementation agreement. Any change to VM semantics – opcode table, control flow, stack depth, type system, or the high-level constraint expansions – must touch four things in the same change: the normative spec/xqvm/ files, the Rust implementation, the Python reference implementation, and the conformance vectors. CI’s atomic-spec-MR guard rejects a change that touches only some of them, unless a commit in the range carries an Atomic-Spec-Exempt: <reason> trailer, which deliberately bypasses it for a one-sided change such as aligning one implementation to the other’s existing behaviour. This keeps the four descriptions of VM behaviour from drifting apart silently by default; it does not keep the behaviour itself from changing, and the exemption is a contributor’s call, not a machine guarantee.

Both guarantee how a change to observable behaviour is made. Neither guarantees that observable behaviour stays fixed.

What Is Not Guaranteed

Nothing here promises binary compatibility across versions. CONTRIBUTING.md states that public API changes must be semver-compatible and that breaking changes require a major version bump. For a 0.x project, that policy governs the crate versions – it is not a commitment that nothing observable will ever break before 1.0. Treat every 0.x release as a snapshot, not a foundation to build on without re-checking. If your use case needs a stability guarantee this project does not yet make, file an issue against the repository rather than assuming one.

Spec Index

The book teaches; spec/ is what XQuad actually is. Every file below is the normative source for its topic’s intent. The conformance harness does not check either implementation against these documents: it checks the Rust and Python implementations against each other and against conformance/opcodes.yaml. The implementation is what ships, and the two can diverge; where a divergence is known, the relevant book page says so. Conformance explains why a green run is evidence about the programs a vector covers rather than a blanket guarantee.

The lists below are not closed. For anything they omit, check the book chapter covering the component you are working with, or search the spec files directly.

Top Level

  • spec/README.md – index of the three layers below, and the drift policy that ties spec to implementation.

XQVM

Normative for the virtual machine: the instruction set, the wire format, and the bytecode verifier.

  • spec/xqvm/README.md – document index for this layer.
  • spec/xqvm/SPEC.md – normative for the machine overview, the three-program architecture, the state model, determinism, the type system, and runtime limits.
  • spec/xqvm/ISA.md – normative for the instruction set: notation, per-category opcode tables, semantic notes, and reserved opcodes.
  • spec/xqvm/HLF.md – normative for the high-level constraint expansion formulas: the QUBO penalty terms ONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, EQUALITY, ATLEAST, ATLEASTW, and REDUCE inject.
  • spec/xqvm/ENCODING.md – normative for both file formats: .xqasm assembly syntax and the .xqb binary encoding, including the XQBC header layout.
  • spec/xqvm/VERIFIER.md – normative for the bytecode verifier: its phase pipeline and every error it can raise.
  • spec/xqvm/METERING.md – normative for step metering: what a step is, the cost constants and how they were measured, the per-opcode charge table, the three formulas, and the conformance rules for step counts and operand validation order.

XQCP

Normative for the constraint-programming DSL that compiles a Problem into the three XQVM programs.

  • spec/xqcp/README.md – document index for this layer.
  • spec/xqcp/SPEC.md – normative for the DSL overview, the three-program architecture as xqcp implements it, the problem lifecycle, and the compilation contract.
  • spec/xqcp/TYPES.md – normative for the symbolic value types, the expression tree, the operator algebra, and the free functions built on top of it.
  • spec/xqcp/CONSTRAINTS.md – normative for the constraint taxonomy, each constraint method’s signature, and its cross-reference into the matching HLF expansion.
  • spec/xqcp/COMPILER.md – normative for the compilation pipeline: encoder/verifier/decoder generation, register allocation, and action recording.

XQSA

Normative for the solver-adapter interface between a model and a backend.

  • spec/xqsa/README.md – document index for this layer.
  • spec/xqsa/SPEC.md – normative for the architecture overview, where solving sits in the pipeline, and the plugin model new solvers implement.
  • spec/xqsa/INTERFACE.md – normative for the Solver abstract class, the solve() contract, the SolverResult type, and parameter-passing conventions.
  • spec/xqsa/ENERGY.md – normative for the energy computation formula, its precision contract, and the sparse representation solvers exchange it in.
  • spec/xqsa/DOMAINS.md – normative for the domain support matrix, sample encoding, grid metadata, and capability negotiation between a model and a solver.
  • spec/xqsa/SOLVERS.md – normative for the solver registry, its naming convention, algorithm families, and the dependency model behind each optional extra.

Reading a Spec File Like a Reference, Not a Tutorial

Every file above assumes the concepts this book explains from scratch – what a quadratic model is, why a problem becomes three programs, what a penalty weight does. Arrive from Concepts or the relevant chapter first; the spec files are where to go once you need the exact rule the book page summarised, not where to start.

Contributing to These Docs

This book is written and reviewed in the same repository as the code it documents. If something here is wrong, incomplete, or out of date, the fix goes through the same review process as a code change. Start from CONTRIBUTING.md for the general contribution rules – sign-off, commit format, and review expectations apply to documentation changes too.

Where the Pages Live

Every page you can read here comes from one Markdown file under docs/book/src/. The URL mirrors the path: this page is appendix/contributing.md.

Two files control the shape of the book rather than its content:

  • docs/book/src/SUMMARY.md is the table of contents and the sidebar. Every page must be linked from it, and every link in it must point at a page that exists. Continuous integration checks both directions, so adding a new page without adding its SUMMARY.md entry fails the build rather than producing an orphan.
  • book.toml, at the repository root, holds the renderer configuration: the theme, the output directory, and the preprocessor list.

Pages You Should Not Edit by Hand

Sixteen pages are generated, not written. Editing one of them directly works until the next regeneration silently reverts it. Each generated file opens with a DO NOT EDIT banner as its first line, which you will see immediately if you open it in an editor.

They are the instruction reference, generated from conformance/opcodes.yaml, and the fifteen example pages, generated from examples/manifest.yaml together with each example’s own README.md.

To change one, edit its source and regenerate:

make regen-docs

Continuous integration runs the same generators in check mode and fails if the committed pages differ from freshly generated output, so a regenerated page has to be committed alongside the source that produced it.

Building the Book Locally

The renderer and its diagram preprocessor are pinned in scripts/cargo-tools.lock and installed by make deps. With those in place:

make build-docs   # build once into docs/book/build/
make serve-docs   # rebuild on save and open a browser

make build-docs also asserts that every diagram in the sources actually rendered, which catches a preprocessor failure that would otherwise show up as a page of raw diagram source.

Two more checks are worth running before you open a merge request:

make check-docs-generated   # generated pages match their sources
make check-docs-drift       # table-of-contents coverage and prose guards

Both run in continuous integration as well, so running them locally just saves you a round trip.

Reporting Something Instead

Not every problem is worth a merge request. If you have found an error but not the fix, or you want to argue that a page should exist at all, open an issue and say which page you mean.