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 vm check checks and how
to fix a rejected program, see Verification.
All three surfaces run the same Rust xqvm interpreter, and every example
on this page runs as shown.
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.
A run that faults raises the xqffi.vm.XqvmError subclass named for the
fault – here StepLimitExceeded – with the fault’s description as its
message. XqvmError is a RuntimeError.
from xqffi.vm import StepLimitExceeded
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 StepLimitExceeded as e:
print(e)
# step charge of 1 exceeds the step limit of 3 (3 steps already charged)
# 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 StepLimitExceeded:
pass
# 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 vm 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: XQBC header is truncated
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 (
Domain,
IndexOutOfBounds,
InvalidGridDimensions,
XqmxModel,
XqmxSample,
)
model = XqmxModel.binary(size=4) # or XqmxModel(Domain.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:
XqmxModel.binary(size=4, rows=2, cols=3)
except InvalidGridDimensions as e:
print(e)
# a 2 x 3 grid does not fit 4 variables; rows and cols must both be 0,
# or both positive with rows * cols <= size
try:
session.set_calldata([object()])
except TypeError as e:
print(e)
# unsupported calldata element type: object; expected int, list[int],
# XqmxModel, XqmxSample, or None
The constructors also refuse a size over
xqffi.vm.MAX_ALLOCATION_SIZE, the largest allocation bytecode can
make, with InvalidAllocation, and XqmxSample refuses a value outside
its domain with ValueError. The coefficient accessors (set_linear,
get_linear, set_quad, get_quad) raise IndexOutOfBounds for a
variable index outside 0..size, a negative index included.
InvalidAllocation, InvalidGridDimensions and IndexOutOfBounds are
XqvmError faults, so RuntimeError subclasses:
try:
model.set_linear(4, 1)
except IndexOutOfBounds as e:
print(e)
# index 4 out of bounds (len 4)
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.add_quad(0, 1, 2)
model.domain # Domain.BINARY
model.domain.name # "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)'
model.energy(XqmxSample.binary([1, 1, 0, 0])) # 1, as ENERGY computes it
add_linear and add_quad accumulate into a coefficient and raise
xqffi.vm.ArithmeticOverflow, leaving the model unchanged, if the result
leaves the signed 64-bit range. A sample’s values are checked against its
domain when it is constructed; XqmxSample.binary([2]) raises
ValueError.
__repr__ is intentionally minimal. To inspect every coefficient or
serialise a model, iterate linear_items() and quadratic_items().
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. .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 wraps xqffi.vm.Vm and runs .xqasm
source text directly, assembling it on each run(); run_bytecode()
takes pre-assembled bytes instead:
from xquad.vm import VM
src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
v = VM()
v.set_output_slots(1)
v.run(src)
assert v.outputs() == [12]
assert v.stack() == []
VM keeps calldata, output slots, and the step and memory limits across
runs until reset(), but resets the machine before every run, so no stack
or register state leaks from one run into the next. VM.outputs() returns
the same XqmxModel / XqmxSample objects as xqffi.vm.Vm.outputs() and
Session.run().outputs. Pick VM when you run .xqasm source from a
script and want the settings to carry across runs; 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.
VM.set_calldata() stores a copy of the list it is given, holding the
same objects, and checks nothing itself, because the values check
themselves. An XqmxSample raises
ValueError when it is built with a value outside its domain, and again
when such a value is written to it, so no out-of-domain sample can reach
run(). The element types are checked at run(): anything other than an
int, a list[int], an XqmxModel, an XqmxSample or None raises
TypeError there. A program that writes an out-of-domain value with
SETLINE or ADDLINE faults with SampleOutOfDomain, an XqvmError.
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.