This file is the authoritative, tool-neutral source of truth for AI coding assistants and human contributors working in this repository. It holds repo-wide conventions for any collaborator.
Tool-specific agent config files (CLAUDE.md, etc.) are not checked into git -- each
developer manages their own locally. Claude Code does not read AGENTS.md natively, so
Claude Code users should add a gitignored CLAUDE.md (or CLAUDE.local.md) whose first
line is @AGENTS.md to auto-load this file. Personal, repository-specific overrides go
in AGENTS.local.md (gitignored), imported at the end of this file and silently skipped
for anyone who does not have one.
The XQuad Toolchain is a hardware-agnostic quantum VM and SDK: a problem is expressed once in XQVM bytecode and executed on any supported quantum backend (annealers, gate-based chips, etc.). Think LLVM for quantum computing. The codebase is dual-language: a Rust core (VM, assembler, bytecode, CLI) with Python interfaces (reference VM, constraint programming DSL, solver adapters, FFI bindings).
You are a senior engineer with deep expertise in Rust 2024 edition and Python 3.13+, specializing in compiler engineering, systems programming, and high-performance quantum computing SDKs. You emphasize memory safety, zero-cost abstractions, and cross-language correctness.
# Full suite (what CI runs)
make all # fmt + lint + test (Rust + Python)
make xquad # bootstrap local dev: Python venv + install xquad CLI
make install-hooks # point git at .githooks/ pre-commit hook
# Preflight (run locally exactly what CI enforces; N/A a language you didn't touch)
make preflight # preflight-rs + preflight-py + preflight-parity + preflight-docs + preflight-policy
make preflight-rs # fmt, taplo, clippy, rustdoc, deny, unit/integration/doc tests
make preflight-py # taplo, ruff format + lint, pytest, uv.lock freshness
make preflight-parity # opcode parity, conformance, example smoke
make preflight-docs # generated-doc freshness + docs drift + README length + prose (needs vale)
make preflight-release # crate packaging dry-run + five Python dists (needs maturin/twine/uv; not in `preflight`)
# Rust
make fmt # cargo fmt + taplo fmt + ruff format
make lint # lint-clippy + lint-doc + lint-deny-rs + lint-py + fmt-check
make lint-clippy # cargo clippy --workspace --all-targets --all-features -- -D warnings
make lint-doc # RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
make lint-deny-rs # cargo deny check
make test # test-unit-rs + test-integ-rs + test-doc + test-py
make test-unit-rs # cargo nextest run --workspace --all-features --lib
make test-integ-rs # cargo nextest run --workspace --exclude xquad-conformance --all-features --test '*'
make test-doc # cargo test --doc --workspace --all-features
make test-miri # cargo +nightly miri test --workspace --all-features
make deps # install rustup components + pinned cargo tools
make deps-miri # install nightly + miri
# Single Rust test by name
cargo nextest run --workspace -E 'test(my_test_name)'
cargo test --workspace my_test_name
# Python
make deps-py # uv sync + maturin develop (editable installs)
make fmt-py # ruff format across all Python packages
make fmt-check-py # ruff format --check
make lint-py # ruff check across all Python packages
make test-py # pytest xqvm_py/tests xqcp/tests xqsa/tests xquad/tests
make check-uv-lock # uv lock --check -- fails if uv.lock is stale against pyproject.toml
make check-xqffi-fresh # uv sync --extra dwave + import xquad -- asserts the xqffi cdylib is
# fresh; mutates .venv/, re-run `make deps-py` afterwards
make repl # Python REPL with xqffi + workspace packages
# Cross-language
make opcode-parity # opcode-parity-rs + opcode-parity-py
make conformance # conformance-rs + conformance-py
make example-smoke # run examples on both interpreters, check valid == 1
# Documentation
make build-docs # mdbook build
make regen-docs # regenerate generated opcode and example book pages
make check-docs-generated # assert generated docs match regenerated output
make check-docs-drift # guard book prose, SUMMARY.md coverage, and page links
make check-docs-mermaid # assert book diagrams rendered (needs make build-docs first)
make check-docs-readme # guard published package READMEs against the 100-line limit
make check-docs-prose # Vale over the handwritten book pages. Needs vale on PATH:
# `brew install vale`; pinned by VALE_VERSION in the Makefile
make serve-docs # mdbook serve --open
# Changelog (CHANGELOG.md is gitignored; cliff.toml + git history is source of truth)
make changelog # generate CHANGELOG.md (preview unreleased)
make changelog-release VERSION=v0.2.0 # preview a tag's release notes (renders exactly one section)
make render-changelog # render-only validation (lint smoke)
make check-release-notes # regression guard: every non-rc release renders exactly one sectionEvery new source file must begin with the AGPL license header. Use // comments for Rust, # comments for Python. In Zed, the agpl snippet (.zed/snippets.json) inserts the Rust header automatically.
Copyright (C) 2026 Postquant Labs Incorporated
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
SPDX-License-Identifier: AGPL-3.0-or-later
Commits must be signed off: git commit -s (DCO requirement from CONTRIBUTING.md).
All commit messages must follow the Conventional Commits format:
<type>(<scope>): <subject>
[optional body]
[optional footer(s)]
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, security, deprecate, release
Scope is optional. When used, it should be the crate or package name (e.g. xqvm, xqcp, conformance).
Rules:
- Subject line: imperative mood, lowercase start, no trailing period, max 72 characters
- Body: wrap at 72 characters, explain what and why
- Footer: reference the tracking issue (e.g.
Fixes #123) to auto-link and close it on merge - Breaking changes: append
!after type/scope (e.g.feat(xqvm)!: remove deprecated API) or add aBREAKING CHANGE:footer - NEVER add
Co-Authored-Bytrailers for AI assistants
Example:
fix(xqasm): handle forward label references in nested loops
The two-pass label resolver was not accounting for label offsets
inside nested RANGE blocks, causing incorrect jump targets when
a forward reference crossed a loop boundary.
Fixes #456
After modifying files, run make fmt to format everything, or the per-file equivalents from the commands section: cargo fmt for Rust, uvx ruff@<pinned version> check --fix <file> + uvx ruff@<pinned version> format <file> for Python (pin lives in the Makefile's RUFF_VERSION), taplo fmt for TOML.
- NEVER use emojis in code, documentation, or commit messages.
- NEVER use em-dash in documentation -- prefer using
--. - Use 4 spaces for indentation (no tabs).
- Aggregate edits to a single file into one pass. Do not thrash multiple small edits to the same file in sequence.
snake_casefor functions, modules, and variables.PascalCasefor types, traits, and classes.- Follow
CONTRIBUTING.mdfor additional Rust conventions and ruff/pycodestyle for Python.
- Safety First: zero
unsafecode unless absolutely necessary. Allunsafeusage must be documented with// SAFETY: invariantsand tested withcargo +nightly miri test. - Idiomatic Rust: follow
CONTRIBUTING.mdfor contribution standards and idiomatic code. - Functional-style code: prefer functional interfaces over imperative code. Use imperative code if functional-style code is less clear.
- Performance: zero-cost abstractions. Be efficient in terms of memory use and performance. Prefer stack allocation over heap.
- Ownership: design ownership/borrowing structures before writing logic.
- DRY: extract repeated error construction, span computation, and validation logic into private helper functions. Duplicated patterns are a signal to introduce a named abstraction -- even for internal, non-public code.
-
Architecture: analyze crates, lifetimes, and public APIs first (methods, traits, etc.). Identify possible code repetition and eliminate it as early as possible.
-
Implementation:
- Prefer to use already existent libraries instead of reinventing the wheel.
- Do not use
unwrap(). Use safer alternatives. - Avoid subscripting or explicit slicing (e.g.
foo[3],bar[0..2]) and useget,get_mutinstead. - Use
panics andasserts only for testing and invariant violations. - Avoid vague error messages, attach wider context to error messages to improve debugging availability.
- Use
miette(eyreishsub-module) for application errors. - Use
clapfor CLIs. - Use
rayonfor CPU-bound tasks that may benefit from parallelism. - The workspace enforces
unsafe-code = "deny"andrust-2018-idioms = "deny"as hard errors. Key warnings that become blocking on CI:indexing-slicing(use.get()/.get_mut()with proper error handling instead of[]),unused-results(must handle or discard withlet _ =).
-
Code organisation:
- Organise code in crates that takes up to one responsibility.
- Every crate should consist of
lib.rs-- facade module that exposes public API by re-exporting other module items. Keep inner modules as private as possible. - Design code using
newtypes rather than type aliases.
-
Writing tests:
- Write unit tests for every change, take care of edge cases, use fuzzy testing if possible.
- Write integration tests in
tests/directory. - Write micro-benchmarks in
benches/directory.
-
Documentation: document every publicly exposed element of API with this format:
/// Short description, up to two sentences: Does this and that. /// /// Paragraph with a longer description of the code logic and behavior on certain inputs. /// /// # Examples /// /// ```rust /// let foo: Foo = Foo::foo(); /// assert!(foo.works_ok()); /// ``` /// /// # Panics /// Description when function panics for unexpected reason. /// /// # Errors /// Description when the function returns a business logic error. /// /// # Safety /// Safety invariants and how the function is safe to use, if marked `unsafe`.
When documenting a publicly exposed module, write a simple description of what the module is doing and how to use code written there. Add examples of how to use the API inside the module.
-
Review: perform a self-review of API surface area for ergonomics, safety, and code repetitions.
-
Validation: run
make lintfor checking lints. Then runmake testto test the code. -
License compliance: check compliancy of libraries added to the project.
- Check whether
cargo denypasses. - If not, check if the license of the library is compatible with
AGPL-3.0-or-later. - If compatible, add the license to
deny.toml. - If not compatible, look for compatible alternatives in
crates.io. - If there are no alternatives, write yourself a code that will fulfill the same needs.
- Update
NOTICEfile accordingly as the library added to the project.
- Check whether
- Focus on reducing complexity in
Cargo.toml. - Optimize for build times (parallel processing, reducing dependencies).
- Ensure high test coverage for edge cases (fuzz testing if necessary).
- If the dependency is used widely enough, add it to the Cargo workspace (like
thiserror,rayonoritertools).
| Crate | Path | Role |
|---|---|---|
xqvm |
xqvm/ |
Bytecode definitions, opcode table, instruction types, builder, codec, stream reader, VM interpreter, disassembler |
xqasm |
xqasm/ |
Text assembler: pest parser -> AST -> bytecode |
xqcli |
xqcli/ |
CLI binary (xquad): asm, dism, run, verify subcommands |
xqffi |
xqffi/ |
PyO3 bindings exposing xqasm + xqvm to Python |
xquad-conformance |
conformance/ |
Cross-implementation conformance harness |
X-Macro opcode table (xqvm/src/bytecode/types/table.rs) -- The opcodes! macro is the single source of truth for all 93 instructions. The Opcode enum, Instruction enum, mnemonic strings, and operand arity are all derived from it. When adding or changing an opcode, edit only this table.
Two-pass label resolution (xqvm/src/bytecode/builder.rs) -- InstructionBuilder records unresolved jump fixups on the first pass and patches offsets at build() time, supporting both forward and backward label references.
Binary codec (xqvm/src/bytecode/codec.rs) -- Uses oxicode with BE fixint encoding: opcode byte followed by operand fields at their natural width in big-endian byte order (i16 = 2 bytes, [u8; N] = N bytes, u8/Register = 1 byte). No varints, no length prefixes. InstructionStream (stream.rs) is an incremental seekable reader over encoded bytes. Mnemonic strings inside the bytecode crate use pastey for no-std compact string storage (avoids heap allocation for fixed-length identifiers).
Assembly pipeline (xqasm/) -- pest grammar -> ast::Program -> assembler::assemble() -> InstructionBuilder -> codec::encode. Rich miette diagnostics with source spans are emitted at the assembler stage.
VM interpreter (xqvm/) -- Vm executes a Program (raw instruction bytes) via an incremental InstructionStream reader. State: 256-slot register file (RegVal enum: Int(i64), VecInt(Vec<i64>), VecXqmx(Vec<XqmxModel>), Model(XqmxModel), Sample(XqmxSample)), an unbounded integer stack, and a loop stack of LoopFrame records (one per RANGE/ITER). StepResult drives control flow: Continue, Jump(offset), Halt, StartLoop. Default step limit is 10,000,000 (configurable via set_step_limit()). A second budget bounds memory: every allocating opcode is charged bytes before it allocates, against a 1 GiB default (configurable via set_memory_limit()), and raises MemoryLimitExceeded when it cannot pay. Calldata and output slots are injected before run() via set_calldata() / set_output_slots(). VM errors carry into_diagnostic(&program, source_name) which disassembles the failing offset for miette source annotation. clippy::result_large_err is explicitly allowed in the asm crate because NamedSource<Arc<str>> on the error path is intentional.
Control flow, stack/register I/O, arithmetic (including SQR, ABS, INC, DEC, MIN, MAX), comparison, logical/bitwise, QUBO/Ising/discrete matrix allocators (BQMX, SQMX, XQMX), sample allocators, vector ops, index math, matrix coefficient access, grid ops, high-level constraints (ONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, EQUALITY, ATLEAST, ATLEASTW, REDUCE), and ENERGY.
- Dependencies: manage via each package's
pyproject.toml. The repo-rootpyproject.tomlhosts theuvworkspace declaration and dev-tool pins (maturin, pytest, pyyaml, ruff); the xqvm_py / xqcp / xqsa / xqffi members carry their own. Never modify the dev-dep pins without explicit user approval. - Virtual environment: always use the workspace
.venv/managed byuv sync/uv run. Never install packages globally or create ad-hoc venvs. Invoke scripts and tests viauv runso the maturin-builtxqffiextension is picked up without a manual activation step. - Setup:
make deps-pyrunsuv sync+maturin develop. Each package's editable install carriesdev-mode-dirs = [".."], which puts the repo root onsys.path. Re-run after pulls that touch Rust sources or workspace deps.
| Package | Path | Role |
|---|---|---|
xqvm_py |
xqvm_py/ |
Python reference VM implementation (conformance oracle) |
xqcp |
xqcp/ |
High-level constraint programming DSL compiling to XQVM assembly |
xqsa |
xqsa/ |
Solver adapters for XQMX models (dwave-samplers; pluggable solver interface) |
xqffi |
xqffi/ |
PyO3 FFI bindings (maturin-built); also a Rust crate |
xquad |
xquad/ |
Umbrella meta-package re-exporting xqffi, xqcp, xqsa under unified namespace |
make test-py runs pytest across xqvm_py/tests, xqcp/tests, xqsa/tests, xquad/tests. Test paths are configured in the root pyproject.toml under [tool.pytest.ini_options].
The spec/ directory contains authoritative specifications for each toolchain component. Read the relevant spec before modifying that component:
spec/xqvm/-- XQVM architecture (opcodes, control flow, type system, encoding)spec/xqcp/README.md-- XQCP constraint programming DSLspec/xqsa/README.md-- XQSA solver adapter interface
Spec changes are governed by the conformance harness: any modification affecting the opcode table, control-flow rules, stack depth, type system, or HLF expansions must be mirrored in conformance/opcodes.yaml and validated against xqvm_py/opcodes.py via scripts/check-opcode-parity.py.
Behavioural parity between xqvm_py (Python reference) and the Rust xqvm crate is enforced by the xquad-conformance test suite. Vectors live in conformance/vectors/. New semantics require a new vector; divergence between impls fails CI with no drift-tracking middle ground.
Any MR that changes VM semantics must touch all four layers in the same MR: (1) spec/xqvm/*.md, (2) xqvm/src/**/*.rs, (3) xqvm_py/{executor,opcodes,xqmx,state,vector,tracer,errors}.py, (4) conformance/vectors/** or conformance/opcodes.yaml. CI enforces this via verify:policy (scripts/check-atomic-spec-mr.sh). MRs touching 0 or all 4 layers pass; partial changes (1-3 layers) fail.
For deliberately one-sided changes (e.g. aligning one impl to existing behaviour), add an Atomic-Spec-Exempt: QUI-<id> <reason> trailer to a commit message. It goes in the message's last paragraph at column 0, beside the sign-off, with the whole reason and the ticket on that one line; git reads trailers from the last paragraph only and truncates a wrapped reason, so the guard fails on either instead of bypassing. A Fixes QUI-NNN footer may share the paragraph but not the line directly below the trailer. The guard scans every commit in the MR range and bypasses when it finds at least one well-formed trailer. See docs/guide/development-workflow.md for the full rationale and exempt cases.
Adding a row to the opcodes! table in xqvm/src/bytecode/types/table.rs is a change to VM semantics. The MR that adds it argues in its description that the new opcode clears all six clauses of the on-chain admissibility bar: no floating point, no host I/O or ambient state, no nondeterministic iteration order, bounded allocation, bounded per-instruction work, and behaviour specified in spec/xqvm/ with a conformance vector covering it. An opcode that cannot clear all six does not ship; the operation belongs in xqcp, xqsa, the xquad API or a helper library instead.
There is deliberately no CI guard for this one -- it is a correctness argument a reviewer weighs, not a string a script can find. docs/guide/development-workflow.md is required reading before changing VM semantics and carries the six clauses in full.
xqvm_py consumes xqffi.asm only -- its executor stays pure-Python so xqvm_py remains an independent conformance oracle. Build with maturin develop --manifest-path xqffi/Cargo.toml (handled by make deps-py).
examples/tsp/ (Travelling Salesman) and examples/maxcut/ (Max-Cut) each consist of .xqasm programs driven by a Python runner (runner.py) that exercises both the Rust and Python interpreters via the --interpreter flag. These are the canonical references for how host code loads and runs .xqasm programs via the toolchain. make example-smoke runs both interpreters and checks each produces a valid solution (valid == 1); the check is invariant-based, not golden-file diffing.
Five phases, each answering one question about the change. verify and
test read as synonyms, so the boundary is stated explicitly rather than
left to be inferred per job -- an unwritten boundary is exactly how the
old lint stage decayed into four unrelated concerns (source formatting,
Rust compilation, cross-implementation parity, release packaging) that
happened to share a stage barrier and nothing else:
verifyasks whether something matches what it is required to match -- a licence against policy, one VM implementation's output against the other's.testasks whether something does what it should when actually executed. Not a static-vs-dynamic split (verify:parityruns the VM); it is consistency between artefacts versus correctness of one artefact.
| Phase | Question it answers | What it covers |
|---|---|---|
verify |
Does the workspace match what it's required to match? | clippy, rustdoc, cargo-deny, ruff, uv.lock freshness, the fresh-xqffi-cdylib check, opcode parity, Rust + Python conformance vectors, example smoke tests, atomic spec-MR guard, commit-message guard, merge-request-title guard, changelog render |
test |
Does the workspace do what it should when executed? | unit, integration, doc tests (Rust); pytest (Python); Quip signing-layer tests; WASM no_std tests; Substrate pallet fixture |
hardware |
Does it work on real hardware? | CUDA, D-Wave QPU, and Metal solver tests on real hardware (protected refs only) |
docs |
Is the documentation correct and buildable? | generated-docs freshness, docs drift guard, package README length guard, mdbook build, GitLab Pages publish |
release |
Is the artefact publishable, and (on a tag) published? | release:validate packaging checks on every pipeline including tags; crates.io + PyPI publishing and GitLab Release notes via git-cliff on a pushed tag |
Gating topology. verify:* and test:* jobs all carry needs: []
and start together at t=0 -- peers, not a chain; a verify failure does
not hold test back. Every hardware:* job then needs: the full
verify+test set, so nothing touches real hardware before the code is
known to compile and pass its own test suite -- a GPU, a metered D-Wave
QPU token, and a macOS runner are all scarce and/or billed, and none of
them should be spent on code that does not even build. docs:* and
release:validate jobs in turn needs: verify+test+hardware, and the
tag-only publish chain (release:crates -> release:pypi ->
release:notes) is gated transitively through release:validate's
needs: edge rather than through the stage barrier -- so the
documentation site and the release path both wait on hardware being
proven, not just on the workspace compiling. The accepted tradeoff: an
offline GPU runner or an expired D-Wave token stalls the documentation
site, even though nothing in the docs content depends on hardware
passing -- we would rather stall the docs than publish a book
describing an opcode or solver behaviour the hardware suite just
proved broken. On a tag the same gate now sits ahead of the publish
chain too, so the same expired token stalls a release and not just the
docs -- test:substrate's cold-cache build is the largest single wait
on that path. The exact graph and per-edge reasoning (including why
hardware:* needs are marked optional: true) live in
.gitlab/ci/setup.yml's "Dependency gating" section.
Path gating. Two jobs do not run on every pipeline. test:wasm and
test:substrate are gated on rules: changes:, because each builds one
crate (xqvm) into one fixture and so has a narrow, writable input
footprint, while every other job in the pipeline is a whole-workspace
check whose verdict a change anywhere can flip. They are the two most
expensive jobs in the pipeline and the least often relevant, so gating
them is most of the merge-request latency available to save. Both stay
unconditional on protected refs and on tags: the gate buys latency, not
coverage, and a path list is a claim about a build graph that can be
wrong -- keeping the protected refs unconditional means a wrong list
costs a late signal on main rather than a shipped regression. Because
either job can be absent, every needs: edge into them is
optional: true; GitLab refuses to create a pipeline whose job needs an
absent job. The path lists, the per-clause reasoning, and the per-entry
justification live in .gitlab/ci/test.yml's "Path gating" section.
Local make preflight-rs runs both targets unconditionally.
Naming. A job's name prefix is its phase (verify:rust runs in the
verify stage, docs:build in docs) -- that is the CI-side taxonomy.
The Makefile stays action-first (<action>-<subject>, e.g. lint-rust,
build-docs) -- that is the local-workflow taxonomy -- so most job names
do not map onto their target mechanically: verify:rust runs make -k lint-rust, docs:build runs make -k build-docs. CI always invokes
these with -k so every prerequisite in a merged job is attempted even
after one fails; a plain local make lint-rust fails fast on the first,
like any other non--k target.
Jobs are authored in per-stage files under .gitlab/ci/ and composed via include: in the root .gitlab-ci.yml.
CHANGELOG.md is not committed to the source tree. The source of truth is cliff.toml plus the conventional-commit log; the file is regenerated on demand via make changelog and published as the GitLab Release description on tag (release:notes in .gitlab/ci/release.yml).
Caveats:
- Pre-conventional-commits history (everything before QUI-480) is filtered out by
filter_unconventional = true; only commits on or after the QUI-480 enforcement appear in the rendered output.make changelogrenders the whole unreleased history with no other scoping, so an empty render is the expected state until the first user-visiblefeat/fixlands. chore,style,test,ci,buildare dropped silently -- if a commit under one of those types ships a user-visible change (e.g. a security-relevant dep bump underchore), promote it tofeat/fix/securitybefore merging or it will be invisible in release notes.cliff.toml'stag_patternscopes each release's notes to that release alone: it keeps an rc tag from ever becoming a range boundary, so an rc's commits fold into the following non-rc release's section instead of getting a page of their own.changelog-release(the Makefile targetrelease:notesinvokes) pairs this with an explicitPREV..VERSIONrange, wherePREVis the nearest non-rc predecessor tag, rather than an unbounded--tag.make check-release-notes(scripts/check-release-notes.sh, run as part oflint-policy/verify:policy) is the regression guard: it renders every non-rc tag's range plus the pre-tag preview and asserts each yields exactly one## [heading.- Before tagging a release, run
make changelog-release VERSION=vX.Y.Zlocally to preview what the GitLab Release page will say -- the render contains exactly one section. Bad commit subjects can be fixed on the source branch and re-merged before the tag is cut.
Personal, repository-specific configuration goes in AGENTS.local.md (gitignored). It is
imported below and silently skipped for collaborators who do not have one.
@AGENTS.local.md