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
Sessiontoday. - Keyword calldata (
session.set_calldata(n=4, ...)).Sessiontakes a positional list only; there is no input-slot labelling to key against. - A
_repr_html_for notebooks.__repr__is the only representationProgram,Session,RunResult,XqmxModelandXqmxSampleprovide. - A
to_numpy()helper. Convert vialinear_items()/quadratic_items()andnumpy.asarray(...)on your own side; this package does not carry a numpy dependency.