diff --git a/.ado/publish.yaml b/.ado/publish.yaml index 64e3f4b1..c8394cb7 100644 --- a/.ado/publish.yaml +++ b/.ado/publish.yaml @@ -23,7 +23,7 @@ resources: parameters: - name: buildAndTest - displayName: 'Build and test binar/paulimer/pauliverse (uncheck to publish only)' + displayName: 'Build and test native packages (required for qodec Python publication)' type: boolean default: true - name: buildAndTestDeqRuntime @@ -131,7 +131,7 @@ extends: - template: stages/build.yaml@self parameters: platforms: ${{ parameters.platforms }} - buildAndTest: ${{ parameters.buildAndTest }} + buildAndTest: ${{ or(parameters.buildAndTest, parameters.publishQodecPython) }} buildDeqRuntime: ${{ or(parameters.publishDeqRuntimePython, parameters.buildAndTestDeqRuntime) }} - template: stages/publish_crate.yaml@self @@ -176,12 +176,6 @@ extends: platforms: ${{ parameters.platforms }} publishPackage: ${{ parameters.publishDeqRuntimePython }} - - template: stages/publish_python_package.yaml@self - parameters: - packageName: qodec - packagePath: qodec/bindings/python - publishPackage: ${{ parameters.publishQodecPython }} - - template: stages/publish_crate.yaml@self parameters: platforms: ${{ parameters.platforms }} @@ -208,6 +202,7 @@ extends: publishBinarPython: ${{ parameters.publishBinarPython }} publishPaulimerPython: ${{ parameters.publishPaulimerPython }} publishDeqagramPython: ${{ parameters.publishDeqagramPython }} + publishQodecPython: ${{ parameters.publishQodecPython }} - template: stages/publish_wasm_wheels.yaml@self parameters: diff --git a/.ado/stages/build.yaml b/.ado/stages/build.yaml index 6fca3535..7ae57789 100644 --- a/.ado/stages/build.yaml +++ b/.ado/stages/build.yaml @@ -52,7 +52,7 @@ stages: variables: arch: ${{ platform.arch }} # Portable RUSTFLAGS shared by test builds AND published wheels so the - # binaries we test match the ones we ship. x86_64 picks the x86-64-v3 + # CPU requirements match. x86_64 picks the x86-64-v3 # microarchitecture level (Westmere AES, AVX2); aarch64 enables the # AES extension that `gxhash` (via `ptr_hash 2`) requires. ${{ if eq(platform.arch, 'aarch64') }}: @@ -67,6 +67,11 @@ stages: condition: succeeded() targetPath: $(System.DefaultWorkingDirectory)/target/wheels artifactName: ${{ platform.name }}-wheels + - output: pipelineArtifact + displayName: "Upload Rust build timings" + condition: succeededOrFailed() + targetPath: $(System.DefaultWorkingDirectory)/target/cargo-timings + artifactName: ${{ platform.name }}-rust-timings steps: - template: _platform_setup_steps.yaml parameters: @@ -76,12 +81,17 @@ stages: displayName: Install cbindgen # the cargo test below requires the shared library to be built first - - script: cargo build -p deq-decoder-reference-plugin --release + - script: cargo build -p deq-decoder-reference-plugin --profile ci-test displayName: Build deq-decoder-reference-plugin cdylib env: RUSTFLAGS: $(rustflagsPortable) - - script: cargo test --workspace --exclude deq-runtime --exclude binar-python --exclude paulimer-bindings --release --all-features + - script: cargo build -p qodec-c --profile ci-test + displayName: Build qodec C libraries + env: + RUSTFLAGS: $(rustflagsPortable) + + - script: cargo test --workspace --exclude deq-runtime --exclude binar-python --exclude paulimer-bindings --profile ci-test --all-features --timings displayName: Build and test all Rust crates (except deq-runtime + Python shims) env: RUSTFLAGS: $(rustflagsPortable) @@ -116,11 +126,12 @@ stages: # agents ship a much newer glibc (Ubuntu 22.04 = 2.35, Azure # Linux 3 = 2.38) and a native link would raise the wheel's # glibc floor above the tag we claim. - pip install "maturin[zig]" pytest hypothesis more-itertools numpy + python -m pip install -r requirements-build.txt pytest hypothesis more-itertools numpy + export CARGO_ZIGBUILD_PYTHON_PATH="$PWD/tools/zig.py" maturin_args+=(--zig --target "$(uname -m)-unknown-linux-gnu" --compatibility $(manylinuxTag)) ;; Darwin) - pip install maturin pytest hypothesis more-itertools numpy + python -m pip install -r requirements-build.txt pytest hypothesis more-itertools numpy ;; *) echo "Unsupported Unix platform: $(uname -s)" >&2 @@ -145,11 +156,15 @@ stages: build_and_test paulimer build_and_test deq/deqagram --import-mode=importlib + maturin build "${maturin_args[@]}" --manifest-path qodec/bindings/python/Cargo.toml --out target/qodec-wheels + python qodec/tools/check_wheel.py target/qodec-wheels + cp target/qodec-wheels/qodec-*.whl "$wheelhouse/" + if [[ "$(uname -s)" == "Linux" ]]; then ls target/wheels/*$(manylinuxTag)*.whl fi ls -la target/wheels/ - displayName: Build + test binar/paulimer/deqagram wheels (Unix) + displayName: Build + test binar/paulimer/deqagram/qodec wheels (Unix) condition: ne(variables['Agent.OS'], 'Windows_NT') env: RUSTFLAGS: $(rustflagsPortable) @@ -161,7 +176,7 @@ stages: python -m venv .venv .\.venv\Scripts\Activate.ps1 pip install --upgrade pip - pip install maturin pytest hypothesis more-itertools numpy + python -m pip install -r requirements-build.txt pytest hypothesis more-itertools numpy $wheelhouse = Join-Path $PWD 'target\wheels' function Build-AndTest([string]$cratePath, [string[]]$pytestArgs = @()) { @@ -181,12 +196,25 @@ stages: Build-AndTest 'paulimer' Build-AndTest 'deq/deqagram' @('--import-mode=importlib') + maturin build --release --strip --manifest-path qodec/bindings/python/Cargo.toml --out target/qodec-wheels + if ($LASTEXITCODE -ne 0) { throw "maturin build failed for qodec" } + python qodec/tools/check_wheel.py target/qodec-wheels + if ($LASTEXITCODE -ne 0) { throw "qodec wheel import check failed" } + Copy-Item -Path 'target/qodec-wheels/qodec-*.whl' -Destination $wheelhouse + dir target\wheels - displayName: Build + test binar/paulimer/deqagram wheels (Windows) + displayName: Build + test binar/paulimer/deqagram/qodec wheels (Windows) condition: eq(variables['Agent.OS'], 'Windows_NT') env: RUSTFLAGS: $(rustflagsPortable) + - bash: | + set -euo pipefail + source .venv/bin/activate + maturin sdist --manifest-path qodec/bindings/python/Cargo.toml --out target/wheels + displayName: Build qodec source distribution + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'x86_64')) + - bash: | source .venv/bin/activate pip install build @@ -251,14 +279,15 @@ stages: pip install --upgrade pip maturin_args=(--release --strip) if [[ "$(uname -s)" == "Linux" ]]; then - pip install "maturin[zig]" + python -m pip install -r requirements-build.txt + export CARGO_ZIGBUILD_PYTHON_PATH="$PWD/tools/zig.py" maturin_args+=(--zig --target "$(uname -m)-unknown-linux-gnu" --compatibility $(manylinuxTag)) # stim-cxx links a prebuilt libstim.a built against libstdc++, # but zig links libc++; mixing the two leaves std::__cxx11 # symbols undefined and the wheel fails at import. export STIM_RS_BUILD_FROM_SOURCE=1 else - pip install maturin + python -m pip install -r requirements-build.txt fi cd deq/deq_runtime @@ -294,7 +323,7 @@ stages: python -m venv .venv .\.venv\Scripts\Activate.ps1 pip install --upgrade pip - pip install maturin + python -m pip install -r requirements-build.txt cd deq\deq_runtime maturin build --release --strip --out ..\..\target\wheels diff --git a/.ado/stages/publish_python.yaml b/.ado/stages/publish_python.yaml index 0e1c850e..598bc39f 100644 --- a/.ado/stages/publish_python.yaml +++ b/.ado/stages/publish_python.yaml @@ -7,12 +7,15 @@ parameters: type: boolean - name: publishDeqagramPython type: boolean + - name: publishQodecPython + type: boolean + default: false stages: - stage: publish_python displayName: Publish Python Packages dependsOn: build - condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'), or(eq(${{ parameters.publishBinarPython }}, true), eq(${{ parameters.publishPaulimerPython }}, true), eq(${{ parameters.publishDeqagramPython }}, true))) + condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'), or(eq(${{ parameters.publishBinarPython }}, true), eq(${{ parameters.publishPaulimerPython }}, true), eq(${{ parameters.publishDeqagramPython }}, true), eq(${{ parameters.publishQodecPython }}, true))) jobs: - job: "Publish_Python_Packages" pool: @@ -48,6 +51,30 @@ stages: displayName: Collect deqagram wheels condition: eq(${{ parameters.publishDeqagramPython }}, true) + - script: | + set -euo pipefail + for platform in "$(System.DefaultWorkingDirectory)/artifacts"/*; do + wheels=("$platform"/qodec-*.whl) + if [[ "${#wheels[@]}" -ne 1 || ! -f "${wheels[0]}" ]]; then + echo "Expected one qodec wheel in $platform" >&2 + exit 1 + fi + destination="$(System.DefaultWorkingDirectory)/target/wheels/$(basename "${wheels[0]}")" + if [[ -e "$destination" ]]; then + echo "Duplicate qodec wheel: $destination" >&2 + exit 1 + fi + cp "${wheels[0]}" "$destination" + done + sdists=("$(System.DefaultWorkingDirectory)/artifacts"/*/qodec-*.tar.gz) + if [[ "${#sdists[@]}" -ne 1 || ! -f "${sdists[0]}" ]]; then + echo "Expected one qodec source distribution" >&2 + exit 1 + fi + cp "${sdists[0]}" "$(System.DefaultWorkingDirectory)/target/wheels/" + displayName: Collect qodec wheels + condition: eq(${{ parameters.publishQodecPython }}, true) + - script: | ls -la $(System.DefaultWorkingDirectory)/target/wheels displayName: List collected wheels diff --git a/.ado/stages/publish_python_package.yaml b/.ado/stages/publish_python_package.yaml index cd0f7569..fadfce50 100644 --- a/.ado/stages/publish_python_package.yaml +++ b/.ado/stages/publish_python_package.yaml @@ -1,6 +1,6 @@ # Builds a committed pure-Python package and publishes it to PyPI via ESRP. # Equivalent to publish_python.yaml but for a single source-built package -# (used by deq and qodec) rather than collected maturin wheels. +# (used by deq) rather than collected maturin wheels. parameters: - name: packageName type: string diff --git a/.ado/templates/build-python-bindings-steps.yaml b/.ado/templates/build-python-bindings-steps.yaml index 6e33b31c..53334932 100644 --- a/.ado/templates/build-python-bindings-steps.yaml +++ b/.ado/templates/build-python-bindings-steps.yaml @@ -7,7 +7,7 @@ steps: - bash: | python -m venv qdk_env source qdk_env/bin/activate - python -m pip install --upgrade maturin pytest hypothesis more-itertools + python -m pip install -r requirements-build.txt pytest hypothesis more-itertools cd binar/bindings/python maturin develop --release cd ../../.. @@ -19,7 +19,7 @@ steps: - script: | python -m venv qdk_env call qdk_env\Scripts\activate.bat - python -m pip install --upgrade maturin pytest hypothesis more-itertools + python -m pip install -r requirements-build.txt pytest hypothesis more-itertools cd binar\bindings\python maturin develop --release cd ..\..\.. diff --git a/.ado/templates/build-wheels-steps.yaml b/.ado/templates/build-wheels-steps.yaml index e8ca4e12..f3fb8830 100644 --- a/.ado/templates/build-wheels-steps.yaml +++ b/.ado/templates/build-wheels-steps.yaml @@ -18,11 +18,12 @@ steps: # Link against Zig's glibc sysroot: the agents ship a newer glibc # (Ubuntu 22.04 = 2.35, Azure Linux 3 = 2.38) and a native link would # raise the wheel's glibc floor above the tag we claim. - python -m pip install --upgrade "maturin[zig]" + python -m pip install -r requirements-build.txt + export CARGO_ZIGBUILD_PYTHON_PATH="$PWD/tools/zig.py" maturin_args+=(--zig --target "$(uname -m)-unknown-linux-gnu" --compatibility manylinux_2_28) ;; Darwin) - python -m pip install --upgrade maturin + python -m pip install -r requirements-build.txt ;; *) echo "Unsupported Unix platform: $(uname -s)" >&2 @@ -42,7 +43,7 @@ steps: condition: ne( variables['Agent.OS'], 'Windows_NT') - script: | - python -m pip install --upgrade maturin + python -m pip install -r requirements-build.txt if not exist target\wheels mkdir target\wheels cd binar\bindings\python maturin build --release --out ..\..\..\target\wheels diff --git a/.github/agents/qodec-bindings.agent.md b/.github/agents/qodec-bindings.agent.md new file mode 100644 index 00000000..888912aa --- /dev/null +++ b/.github/agents/qodec-bindings.agent.md @@ -0,0 +1,43 @@ +--- +description: 'Reviews and evolves the qodec Python bindings (PyO3) API surface — investigates, proposes options, implements across Rust + .pyi stubs + tests, and verifies the full toolchain.' +tools: ['edit', 'search', 'runCommands', 'runTasks', 'usages', 'problems', 'testFailure', 'todos'] +--- + +# qodec Bindings API Reviewer + +You evolve the **qodec Python bindings** (`qodec/bindings/python/`) — the PyO3 0.29 wrapper +over the Rust core — with an emphasis on a clean, Pythonic, internally-consistent API +surface. You work in deliberate, reviewable increments. + +Read the [shared instructions](../instructions/qodec.instructions.md) and +[Python guidance](../instructions/qodec-python.instructions.md) before work. +For model changes, also read the [model guidance](../instructions/qodec-model.instructions.md). +These files own compatibility and API conventions; do not duplicate them here. + +## Operating principles + +- **The Rust core (`qodec/src/`) is the source of truth.** Prefer extending/fixing the core + over duplicating logic in the bindings. Only reshape `qodec/src/` when the user explicitly + asks for a behavior change, not merely a binding ergonomics tweak. +- **Keep the affected surfaces in sync** using the shared instructions, including + the C ABI when model changes affect it. +- **Avoid over-engineering.** Do not add speculative accessors, typed introspection, or + abstractions that have no consumer. If introspection is test-only, question whether it + should exist at all. Prefer the smallest honest surface. +- **Compatibility.** Follow the changelog's compatibility contract and release + rules from the shared instructions; pre-1.0 does not waive them. + +## Workflow for each review item + +1. **Investigate first.** Read the relevant Rust wrapper, the `.pyi` stub, the Rust core + type it wraps, and any consumers (`qodec/tests/`, `qodec/examples/`, and downstream packages) + before proposing anything. Confirm claims against the code — do not assume. +2. **Present options, then let the user decide.** For non-trivial changes, lay out 2–4 + concrete options with honest tradeoffs and a recommendation. Wait for the decision. +3. **Implement across all layers** in one pass: Rust wrapper + `.pyi` stub + tests + + docstrings (+ schema/docs if the on-disk shape moved). +4. **Verify** using [qodec-checks.instructions.md](../instructions/qodec-checks.instructions.md). + Run focused checks first and the full gates before merge. Use its check runner + with the existing selected interpreter; do not maintain separate commands here. +5. **Report concisely.** Summarize what changed and why, note the verification result, + and offer the next item. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5ae5f5d2..25e8b245 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,4 +20,10 @@ - Benchmarks used to compare packages should typically use deterministic inputs for reproducibility - When adding new functionality, include relevant unit tests and documentation. - Avoid inline commments. They are usually redundant and often wrong. Prefer clear code and good variable names instead. -- Avoid abbreviations and single-letter variable names, unless they very clearly improve clarity. \ No newline at end of file +- Avoid abbreviations and single-letter variable names, unless they very clearly improve clarity. + +## qodec + +For work under [qodec/](../qodec/), follow the scoped +[qodec instructions](instructions/qodec.instructions.md). They link to the model, +Python binding, and verification guidance; do not apply those rules to sibling crates. \ No newline at end of file diff --git a/.github/instructions/qodec-checks.instructions.md b/.github/instructions/qodec-checks.instructions.md new file mode 100644 index 00000000..d62c41b4 --- /dev/null +++ b/.github/instructions/qodec-checks.instructions.md @@ -0,0 +1,171 @@ +--- +description: 'Use before qodec builds, tests, lint, type checking, stubtest, coverage, documentation checks, CI debugging, or pre-merge verification. Includes environment prerequisites and working directories.' +--- + +# qodec Checks + +[The parent build workflow](../workflows/build.yaml) runs qodec's +verification job on Python 3.11 and 3.12, with coverage, language docs, and +packaging checked on the 3.12 leg. These gates invoke +[tools/check.py](../../qodec/tools/check.py). The parent's cross-platform workspace +tests also cover qodec's Rust crates and build the C library first. +GitHub and Azure shared Rust test commands use the workspace's `ci-test` +profile (release settings with LTO off), including the C and reference-plugin +prebuilds. Wheel builds stay on `release`; the qodec-specific runner retains its +development-profile Rust gates. Do not mix profiles for tests and their prebuilds. +[The wheels workflow](../workflows/qodec-wheels.yaml) builds and +imports native Linux, Windows, and universal2 macOS wheels without publishing. +The parent [Azure build stage](../../.ado/stages/build.yaml) builds and +probes qodec wheels on all six native platforms, plus an sdist on Linux x86_64. +Publication uses the parent's [manual ESRP pipeline](../../.ado/publish.yaml), +as described in [RELEASING.md](../../qodec/RELEASING.md); do not run it to check code. +Azure retains a `-rust-timings` artifact from the workspace test build, +including after test failures. Use that report to investigate compile time before +changing release optimization settings. + +The `packaging` scope discovers every `tools/test_*.py` test, including runner +and formatter tests. Keep qodec-specific verification commands in the runner; +workflow files own environment setup and platform selection. Match the parent +Clippy policy (`-D clippy::pedantic`); do not add a workspace lint table. +Packaging tests require `packaging` for Rust-to-PEP-440 version comparison. +Native-wheel validation invokes `tools/check_wheel.py` to install and probe the +wheel outside the checkout. Azure runs that probe on native ARM64 hosts as well. + +## Repeatable Commands + +Use the existing selected interpreter to run [tools/check.py](../../qodec/tools/check.py) +with scope `rust`, `python`, `docs`, `coverage`, `examples`, `packaging`, or `all`. +For example, from the qdk-ec repository root: + +```bash +python qodec/tools/check.py python +python qodec/tools/check.py all --dry-run +``` + +Replace `python` with the selected executable, not an assumed terminal alias. +The runner sets child-process working directories and builds against the invoking +interpreter, leaving the rest of the environment alone. It prints executable paths, +verifies the rebuilt package imports from this checkout, and stops at the first +failed gate. `--dry-run` prints commands without running checks or installing tools. + +The root [VS Code tasks](../../.vscode/tasks.json) expose the same scopes using +`${command:python.interpreterPath}`. Prefer the runner or these tasks to recreating +command chains in agents or chat. Neither requires checked-in interpreter settings. +The gate descriptions below explain what runs; individual commands remain available +for focused work. + +## Environment + +- Check the selected Python interpreter and available compiler/linker before + running builds. Use the selected Python environment for `maturin`, `mypy`, + `stubtest`, `pytest`, and Sphinx; do not silently replace it or assume a + machine-specific conda environment. Ensure imports resolve to this checkout. +- Activate the environment you intend to use, or invoke its interpreter directly. + `maturin develop` installs into the interpreter running it unless `VIRTUAL_ENV` + or `CONDA_PREFIX` names another one, so avoid conflicting conda and virtualenv + selections. +- Install native wheel tools from [requirements-build.txt](../../requirements-build.txt) + using the selected interpreter. Check that `python -m maturin --version` agrees + with its installed package metadata. Linux Zig builds use the shared + [adapter](../../tools/zig.py) through `CARGO_ZIGBUILD_PYTHON_PATH`; see + [native wheel build tools](../../CONTRIBUTING.md#native-wheel-build-tools). +- Display tests use `PyYAML`, `types-PyYAML`, and `ipython`, installed by CI + alongside `pytest` and `mypy`. These are test tools, not runtime dependencies. +- Binding tests, documentation examples, and example audits need `stim>=1.16,<2`, + supplied by the optional `qodec[parsers]` extra. The base package has no mandatory + Stim dependency; missing-dependency tests must still exercise that boundary. +- Rust builds need a working linker, including for build scripts and procedural + macros during `cargo check` or `clippy`. Python binding builds also need the + selected Python interpreter available to PyO3. +- The C header consistency test needs `cbindgen`. Coverage needs `cargo-llvm-cov` + and the Rust `llvm-tools-preview` component. Python docs dependencies are in + [requirements.txt](../../qodec/bindings/python/docs/requirements.txt). +- The Python binding crate is an extension module with no Rust test target: an + interpreter loads it, so its behavior is covered by `pytest`. No library-path + setup is needed for `cargo test`. + +## Focused Development Checks + +- After a change, run the smallest relevant test or content check first. For + Rust changes, use the owning crate and test filter; for Python changes, use the + affected test file. Narrow checks do not replace the full pre-merge gates. +- Compare normalized filesystem paths with Rust `Path` or Python `pathlib.Path`, + not separator-specific strings. Keep literal comparisons for authored YAML keys + and preserved source text. Write raw-text fixtures without platform newline + translation when their exact contents matter. +- Rebuild the Python extension using the build step below before `pytest` or + `stubtest` when Rust core or binding changes affect it. Follow + [qodec-python.instructions.md](qodec-python.instructions.md) for stub and + runtime-value coverage. +- Report exactly which checks ran and any blocked or unverified gates. + +### Example Audit + +`python qodec/tools/check.py examples` runs `qdk.ec.audit` on every retained example. +It also runs in `all`. Use a compatible QDK build with its `ec` dependencies; +the suite never skips for a missing audit engine. Every error and every new or +increased warning fails. Only the per-example unsupported-verification warnings +documented in [examples/README.md](../../qodec/examples/README.md#correctness-tests) +are allowed. No pipeline installs this development QDK build yet, so a green CI +run is not evidence that the examples passed audit. + +## Full Pre-merge Gates + +All qodec gates must pass before merge. Run the Rust commands from the qdk-ec +repository root; package selection must not include sibling crates: + +```bash +cargo fmt -p qodec -p qodec-python -p qodec-c -- --check +cargo clippy -p qodec -p qodec-python -p qodec-c --all-targets --all-features -- -D clippy::pedantic +cargo build -p qodec-c +cargo test -p qodec -p qodec-python -p qodec-c --all-features +``` + +Keep all three packages selected. Build the C staticlib before its Linux smoke +tests, using the same profile and target as the tests. The Rust ABI and generated +[header](../../qodec/bindings/c/include/qodec.h) checks run on all CI platforms; +compiled C callers are qualified only on Linux. Python runtime behavior is +tested through `pytest`, not the Rust suite. + +From [qodec/bindings/python/](../../qodec/bindings/python), with the Python environment set: + +```bash +maturin develop --release +mypy python/qodec tests +python -m mypy.stubtest qodec --allowlist stubtest-allowlist.txt +python -m pytest -q +``` + +Stubtest must check both missing runtime members and missing stub declarations, +including module exports. Keep its allowlist limited to the documented type-only +aliases; do not suppress all runtime members missing from stubs. + +### Language Documentation + +From the qodec directory, with the Python docs dependencies installed: + +```bash +RUSTDOCFLAGS="-D warnings" cargo doc -p qodec -p qodec-python -p qodec-c --no-deps +python -m sphinx -W --keep-going -b html bindings/python/docs target/python-docs/html +python -m sphinx -W --keep-going -b doctest bindings/python/docs target/python-docs/doctest +python bindings/python/docs/test_docs.py +``` + +### Coverage + +From the qdk-ec repository root, enforce the core-only CI line-coverage floor: + +```bash +cargo llvm-cov -p qodec --summary-only --fail-under-lines 88 +``` + +Keep `-p qodec`: sibling crates and the bindings must not affect the core's 88% +floor. Use a fresh report, not a recorded percentage. The Rust report does not +measure Python wrapper execution inside the extension. Use +[tools/binding-coverage.sh](../../qodec/tools/binding-coverage.sh) to measure that work. +Run it in the selected Python environment with `llvm-tools-preview` installed +for the active Rust toolchain. It uses that compiler's LLVM tools and reports +only `bindings/python/src/`, excluding dependencies from `TOTAL`. Python adapter +code and native Rust tests are outside this measurement. It leaves an +instrumented debug extension installed; restore the release build using the +Python build step above afterwards. \ No newline at end of file diff --git a/.github/instructions/qodec-model.instructions.md b/.github/instructions/qodec-model.instructions.md new file mode 100644 index 00000000..a0e2a071 --- /dev/null +++ b/.github/instructions/qodec-model.instructions.md @@ -0,0 +1,104 @@ +--- +description: 'Use when changing or reviewing qodec model behavior, loading, persistence, validation, circuit calls, parity references, navigation, schemas, examples, or documentation, including through Python or C bindings.' +applyTo: 'qodec/src/**,qodec/schemas/**,qodec/tests/**,qodec/examples/**,qodec/docs/concepts/**' +--- + +# qodec Model + +Read the owning Rust implementation before changing its contract. Loading and +resolution live in [qodec/src/qodec/](../../qodec/src/qodec); operation-specific guards live +in [validation.rs](../../qodec/src/validation.rs). + +## Loading and Persistence + +- Rust `Qodec::load`, Python `Qodec.load`, and C `qodec_load` take an explicit + manifest or bundle file path, never a directory. Save takes a destination + directory and returns the written manifest path; pass that result to load. +- Directory saves reuse unchanged filesystem artifacts outside the original + manifest directory, after checking their loaded text before any writes. + Edited artifacts and documents needing new references get local copies. + External input files must never be overwritten. Project-local files copy + normally; bundles copy all current values and do not reuse external files. + Bundle entries are not filesystem origins. Loading records are comparison + baselines, never substitutes for current values. No symlink or race protection + is promised. Keep generated artifact names from redirecting output paths. +- Reference fields determine artifact types: layer `instruction_set`, `codes`, + and `gadgets`, then each gadget's external `checks`, `readouts`, and circuit + `source`. Read only referenced content. Do not scan directories, classify + artifacts by suffix, or reject unreferenced files and bundle entries. +- Resolve paths relative to the containing manifest or gadget document, including + its key inside a bundle. Preserve leading `..` components. Example directory + layouts and artifact filename suffixes are conventions, not requirements. +- A bundle starts with a single-entry `{arbitrary-key: manifest}` envelope. Its + key is unrestricted; remaining entries are looked up by referenced path. + Circuit-source entries may contain raw text. +- Manifest `gadgets` values reference YAML gadget documents, never raw circuits. + A circuit-only gadget is a document containing, for example, + `circuit: ./idle.stim`. +- Read `CURRENT_SCHEMA_VERSION` for the current version. Update explicit versions + in fixtures and generators when the format changes; retain old versions only + in deliberate compatibility-rejection tests. Keep fixture paths explicit. + +## Circuit Source and Calls + +- Loading and saving preserve circuit source without interpreting calls, + including inline YAML lists, invalid text, and unknown languages. Explicit + `format` tags inline text; untagged strings are file references. +- Language identification is separate from artifact typing. Preserve the source + extension and inline `format` rules; do not imply that arbitrary languages can + be parsed. `Circuit.calls`, `blocks`, and `readouts` parse on request and may + fail on a loadable draft. Audit owns call validity. The C projection retains + source and reports parse failures per circuit. +- `Circuit.blocks` returns distinct block labels in first-appearance order, + without expanding multi-qubit blocks. It remains a read-only Python property. +- Python `Circuit.calls()` is a method with a keyword-only `parser` override. + `qodec.register(parser, format="tag")` installs a PyO3 callback wrapper in the + shared Rust registry; no Python registry exists. Latest registration wins + across both languages in the same linked Rust instance. YAML is registered + directly in Rust. Python callbacks get an ISA snapshot and return complete + call values, require a live interpreter, and are released at shutdown. + The optional Stim adapter is registered at module load only if Stim is available. + Optional Python Stim uses the official parser. Rust has `register` and + `*_with` overrides but no default Stim parser. Configuration is not model data. +- Before changing YAML call parsing, read + [inline_yaml_parser.rs](../../qodec/src/inline_yaml_parser.rs). The object form is + `{mnemonic: {operands: [...], arguments: {...}, select: [...]}}`. All three + fields default to empty; reject unknown fields. The list form is shorthand + for block operands followed by named arguments, with no reserved argument + names. Per-call selection exists only in the object form. Save preserves the + authored form. +- Scalar `true` and `false` are `Argument::Boolean(bool)`, distinct from integers, + in both call forms. Quoted values remain strings. Reject booleans in arrays, + block operands, and selection bits. `select` is an OR list of patterns, never + a bare map; each pattern is an AND of integer `0`/`1` flag values. + +## References and Navigation + +- Before changing parity or gadget code or documentation, read the module docs + in [parity.rs](../../qodec/src/parity.rs) for the reference grammar and readout roles. + Equations are flat lists of references to bits and signs relative to the gadget + root; an equation does not by itself assert that its parity is zero. +- Encoding references are positional, for example `in[0].stabilizers[1]`. + Never author the rejected named-operand form `in.target.stabilizers[0]` or + `in: {target: ...}`. Reference slices and unions expand to multiple atoms. +- Before changing `Qodec.resolve`, `Node`, or source locations, read + [paths.md](../../qodec/docs/concepts/paths.md). Model navigation follows resolved + declarations and never implicitly parses circuits. It is distinct from parity + references. Keep `Node` opaque, with no collection dunders; Python truth tests + raise. Source locations are optional loaded-revision points, not protocol data. + +## Validation Boundaries + +- Keep field/reference syntax, code Pauli syntax needed for dimensions, map-name + uniqueness, resolved artifacts, layer bindings, encoding alignment, and derived + readout roles as operation-specific guards. Bottom-layer gadgets require a + target layer. +- Empty/single-layer drafts, unequal X/Z lists, partial readouts, out-of-bounds + parity references, invalid parameter uses/actions, and invalid circuit text + can persist. Algebra and protocol correctness belong to `qdk.ec.audit`. + Analysis routines retain their own preconditions. +- Do not export `ValidationIssue` or `validation_issues()` as a general diagnostic + API. Do not reject loadable drafts merely because audit would report errors. +- Omitted observable and flag equations are undefined, not implicit zero bindings; + both are audit errors. An explicit `[]` equation declares zero. A supplied + partial list of readout equations is also an audit error. \ No newline at end of file diff --git a/.github/instructions/qodec-python.instructions.md b/.github/instructions/qodec-python.instructions.md new file mode 100644 index 00000000..2ac6263d --- /dev/null +++ b/.github/instructions/qodec-python.instructions.md @@ -0,0 +1,43 @@ +--- +description: 'Use when changing or reviewing qodec Python bindings, PyO3 wrappers, .pyi stubs, exports, Python value semantics, binding tests, or Python API documentation.' +applyTo: 'qodec/bindings/python/**' +--- + +# qodec Python Bindings + +The PyO3 wrappers are in [qodec/bindings/python/src/](../../qodec/bindings/python/src); +the package and hand-written stubs are in +[qodec/bindings/python/python/qodec/](../../qodec/bindings/python/python/qodec). +Keep wrappers thin. Read [qodec-model.instructions.md](qodec-model.instructions.md) +for changes to model behavior, even if only binding files are edited. + +## Signatures and Exports + +- `.pyi` stubs are the canonical Python signature source. Update them alongside + binding changes, together with tests and docstrings. +- Each stub mirrors its own module. The top-level stub covers curated top-level + exports; the `codes`, `gadgets`, and `instructions` stubs describe their own + modules. Do not declare a name at the top level unless it resolves there at + runtime. The `qodec.instructions` module name remains unchanged. +- PyO3 constructors are `__new__`, not `__init__`; PyO3 classes are `@final`. +- Optional constructor arguments are keyword-only: use `*` in the PyO3 signature. +- Value types implement structural `__eq__` and are then unhashable under PyO3. + Follow the separate model-navigation contract for `Node` identity and hashing. +- Name-keyed collections such as `Layer.gadgets` and + `InstructionSet.instructions` accept a `list` or `dict` and return a `dict` + keyed by mnemonic. + +## Verification + +- Read [qodec-checks.instructions.md](qodec-checks.instructions.md) for commands + and working directories. Rebuild the extension before runtime checks whenever + Rust core or binding changes affect it; an installed binary can be stale. +- Keep the package's PEP 561 `py.typed` marker. Without it, stub checking can + report success without comparing the shipped stubs. +- Run `stubtest` for binding or signature changes. It checks signatures, not + runtime values; test value-shape changes such as `int` to `bool` with `pytest`. +- Keep [stubtest-allowlist.txt](../../qodec/bindings/python/stubtest-allowlist.txt) + minimal. It is for type-only aliases with no runtime counterpart by design, + not for hiding binding/stub mismatches. +- Python API docs use Sphinx AutoAPI over `.pyi` files. Check generated docs and + doctests when changing stubs or usage documentation; do not commit the output. \ No newline at end of file diff --git a/.github/instructions/qodec.instructions.md b/.github/instructions/qodec.instructions.md new file mode 100644 index 00000000..40952b80 --- /dev/null +++ b/.github/instructions/qodec.instructions.md @@ -0,0 +1,73 @@ +--- +description: 'qodec: shared architecture, ownership, compatibility, and task routing.' +applyTo: 'qodec/**' +--- + +# qodec + +qodec models quantum error-correction protocols: codes, logical instructions, and +gadgets that lower a source instruction set to a target instruction set. The +qodec package includes the Rust core, Python bindings, and C bindings. It is +pre-1.0; every 0.x minor may break the API and on-disk format. + +## Shared Rules + +- The Rust model in `qodec/src/` is canonical. Implement behavior there; keep the + Python and C bindings thin translation layers. +- qodec preserves declarations; `qdk.ec.audit` checks protocol correctness. Retain + only operation-specific guards against ambiguity, information loss, or + unavailable interpretation. Do not add a second general diagnostic API. +- For model changes, check the Rust types, JSON schemas, Python wrappers and + stubs, C ABI and generated header, documentation, examples, and tests together. +- The Rust FFI source and `cbindgen` configuration own the generated + [C header](../../qodec/bindings/c/include/qodec.h); never edit it by hand. Keep shared + model concepts aligned across Rust, Python, and C while using C-native shapes. + Preserve explicit ABI version, size, and offset tests, and regenerate the header + with [regenerate.sh](../../qodec/bindings/c/regenerate.sh) after source changes. +- Core loaders and parsers return `Result` with typed errors. Reserve `panic!` + and `unreachable!` for tests and genuinely unreachable invariants. +- Use `Layer.instruction_set`, `Circuit.instruction_set`, and `Circuit.calls` + (Rust `calls()`). An instruction set has sibling `blocks` and `instructions`. + Do not restore the old `isa` field or use `Circuit.instructions` for calls. +- Breaking on-disk changes bump `schema_version` under the compatibility contract + in [CHANGELOG.md](../../qodec/CHANGELOG.md). Additive changes and fixes rejecting + already-invalid documents do not. Update affected schemas and documentation. +- Each qodec crate declares its own `[package].version`; all three belong to + the `qodec` cargo-release group, independently of `schema_version`. Use the + scoped `cargo release version` commands in [RELEASING.md](../../qodec/RELEASING.md), + never hand-edit version strings or bump sibling qdk-ec packages. +- Private design drafts are not part of this public source tree. Do not copy + them into documentation or release artifacts without publication approval. +- Do not commit generated documentation. Language guidance belongs in rustdoc, + Python stubs and usage docs, or the [C binding guide](../../qodec/bindings/c/README.md). +- Before comparing qodec with QDK, deq, Stim, or another package, verify the + claimed behavior against current source or documentation for the version under + discussion. Do not infer another package's behavior from an absent qodec API or + concept. +- In introductory and ecosystem documentation, explain what the reader can do + and why before cataloging fields. Introduce qodec terms when they are needed, + with a concrete example; do not assume knowledge of an older format or of the + distinctions among blocks, operands, parameters, arguments, encodings, and + readouts. +- When the user approves a batch of review findings, track every accepted item + through implementation and relevant checks, then complete any requested + independent review pass before reporting. Pause only for a blocker, scope + expansion, or an unresolved design decision. + +## Read Before the Relevant Task + +Read only the guidance required by the task. These rules also apply to reviews +and cross-language work, even when the edited file does not match a scoped glob. + +- Before changing model behavior, loading, validation, circuit calls, references, + navigation, schemas, or model examples/docs, read + [qodec-model.instructions.md](qodec-model.instructions.md). This includes model + changes made through a Python or C binding. +- Before Python binding, stub, export, or binding-test work, read + [qodec-python.instructions.md](qodec-python.instructions.md). +- Before builds, tests, lint, coverage, documentation checks, or pre-merge + verification, read [qodec-checks.instructions.md](qodec-checks.instructions.md). +- Before version or release work, read [RELEASING.md](../../qodec/RELEASING.md), including + its prerelease rules. Releases go through a PR; tag the merged commit. + +Keep the relevant instruction file current when a convention or CI check changes. \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a29405c6..5260b188 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -81,7 +81,7 @@ jobs: python -m venv qdk_env source qdk_env/bin/activate python -m pip install --upgrade pip - pip install maturin pytest pytest-asyncio hypothesis more-itertools numpy + python -m pip install -r requirements-build.txt pytest pytest-asyncio hypothesis more-itertools numpy - name: Create Python virtual environment and install dependencies (Windows) if: runner.os == 'Windows' @@ -90,7 +90,7 @@ jobs: python -m venv qdk_env call qdk_env\Scripts\activate.bat python -m pip install --upgrade pip - pip install maturin pytest pytest-asyncio hypothesis more-itertools numpy + python -m pip install -r requirements-build.txt pytest pytest-asyncio hypothesis more-itertools numpy - name: Build binar Python bindings (Linux/Mac) if: runner.os != 'Windows' @@ -262,9 +262,10 @@ jobs: # The reference-plugin cdylib is loaded by path in the ABI integration # tests and the deq-runtime dynlib test; `cargo test` does not build a # cdylib artifact, so build it before any test that loads it. - cargo build --release -p deq-decoder-reference-plugin - cargo test --workspace --exclude deq-runtime --all-features --release - cargo test --package deq-runtime --features "cli simulator tesseract python dylib" --release + cargo build --profile ci-test -p deq-decoder-reference-plugin + cargo build --profile ci-test -p qodec-c + cargo test --workspace --exclude deq-runtime --all-features --profile ci-test + cargo test --package deq-runtime --features "cli simulator tesseract python dylib" --profile ci-test # Run pytest from inside deq/ so that the implicit '' entry in # sys.path resolves to a directory that contains the real # deq/__init__.py. From the repo root, Python would otherwise @@ -282,9 +283,10 @@ jobs: shell: cmd run: | call qdk_env\Scripts\activate.bat - cargo build --release -p deq-decoder-reference-plugin - cargo test --workspace --exclude deq-runtime --all-features --release - cargo test --package deq-runtime --features "cli simulator tesseract python dylib" --release + cargo build --profile ci-test -p deq-decoder-reference-plugin + cargo build --profile ci-test -p qodec-c + cargo test --workspace --exclude deq-runtime --all-features --profile ci-test + cargo test --package deq-runtime --features "cli simulator tesseract python dylib" --profile ci-test REM See the Linux/Mac step for why pytest must run from deq/. pushd deq && pytest tests && popd pushd binar\bindings\python && pytest tests && popd @@ -307,3 +309,78 @@ jobs: cargo bench --package binar --no-run cargo bench --package paulimer --no-run cargo bench --package deq-runtime --no-run + + qodec: + name: qodec - Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + RUSTFLAGS: "-C target-cpu=x86-64-v3" + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12"] + include: + - python-version: "3.12" + full: true + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt, llvm-tools-preview + + - name: Cache cargo registry and index + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-qodec-registry-${{ hashFiles('**/Cargo.toml') }} + + - name: Cache cargo build + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: target + key: v2-${{ runner.os }}-qodec-target-${{ matrix.python-version }}-${{ env.RUSTFLAGS }}-${{ hashFiles('**/Cargo.toml') }} + + - name: Install cbindgen + run: cargo install cbindgen --locked + + - name: Install coverage tooling + if: matrix.full + run: cargo install cargo-llvm-cov --locked + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Create qodec virtual environment + run: | + python -m venv qodec_env + echo "$PWD/qodec_env/bin" >> "$GITHUB_PATH" + + - name: Install qodec check dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-build.txt mypy pytest packaging PyYAML types-PyYAML ipython "stim>=1.16,<2" + python -m pip install -r qodec/bindings/python/docs/requirements.txt + + - name: qodec Rust gates + run: python qodec/tools/check.py rust + + - name: qodec Python binding gates + run: python qodec/tools/check.py python + + - name: qodec documentation gates + if: matrix.full + run: python qodec/tools/check.py docs + + - name: qodec core coverage floor + if: matrix.full + run: python qodec/tools/check.py coverage + + - name: qodec packaging gates + if: matrix.full + run: python qodec/tools/check.py packaging diff --git a/.github/workflows/qodec-wheels.yaml b/.github/workflows/qodec-wheels.yaml new file mode 100644 index 00000000..68b46cb3 --- /dev/null +++ b/.github/workflows/qodec-wheels.yaml @@ -0,0 +1,75 @@ +name: qodec-wheels + +on: + pull_request: + branches: [main] + paths: + - "qodec/**" + - "**/Cargo.toml" + - "requirements-build.txt" + - ".github/workflows/qodec-wheels.yaml" + push: + branches: [main] + paths: + - "qodec/**" + - "**/Cargo.toml" + - "requirements-build.txt" + - ".github/workflows/qodec-wheels.yaml" + workflow_dispatch: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + wheel: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - name: Linux x86_64 + os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - name: Windows x86_64 + os: windows-latest + target: x86_64-pc-windows-msvc + - name: macOS universal2 + os: macos-latest + target: universal2-apple-darwin + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Add macOS targets + if: runner.os == 'macOS' + run: rustup target add x86_64-apple-darwin aarch64-apple-darwin + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Install maturin + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-build.txt + + - name: Build wheel + working-directory: qodec/bindings/python + run: maturin build --release --target ${{ matrix.target }} --out ../../../target/wheels + + - name: Install and import the wheel + run: python qodec/tools/check_wheel.py target/wheels + + - name: Upload wheel + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: qodec-wheel-${{ matrix.target }} + path: target/wheels/qodec-*.whl \ No newline at end of file diff --git a/.github/workflows/wasm-wheels.yaml b/.github/workflows/wasm-wheels.yaml index 737af3f6..7b1beedb 100644 --- a/.github/workflows/wasm-wheels.yaml +++ b/.github/workflows/wasm-wheels.yaml @@ -13,8 +13,9 @@ name: wasm-wheels # # deq-runtime is intentionally excluded: it pulls in C++ (stim, tesseract via # cxx), a multi-threaded tokio runtime and rayon, none of which build for the -# single-threaded Emscripten target. deq and qodec are pure Python and already -# work in Pyodide via their normal universal wheels. +# single-threaded Emscripten target. The deq frontend is pure Python. +# qodec includes a native Rust extension; this workflow does not build or test +# qodec WASM wheels, so its native wheels do not establish Pyodide support. on: pull_request: diff --git a/.gitignore b/.gitignore index 86772d42..b781f0c5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ Cargo.lock **/*.rs.bk .vscode +!/.vscode +/.vscode/* +!/.vscode/tasks.json *.asm diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..2787e8a9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,68 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "qodec: check all", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "all"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + }, + { + "label": "qodec: check Rust", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "rust"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + }, + { + "label": "qodec: check Python", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "python"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + }, + { + "label": "qodec: check docs", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "docs"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + }, + { + "label": "qodec: check coverage", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "coverage"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + }, + { + "label": "qodec: check examples", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "examples"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + }, + { + "label": "qodec: check packaging", + "type": "process", + "command": "${command:python.interpreterPath}", + "args": ["${workspaceFolder}/qodec/tools/check.py", "packaging"], + "options": {"cwd": "${workspaceFolder}"}, + "group": "test", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3028494..5dd479a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,58 @@ Python bindings should: - Include type hints (`.pyi` files) - Have corresponding tests in the `tests/` directory +For qodec-specific build commands, verification gates, and format compatibility, +see the [qodec development guide](qodec/CONTRIBUTING.md). + +## Rust CI Test Profile + +GitHub and Azure use `--profile ci-test` for their shared Rust test jobs. +This profile inherits `release` but disables LTO across the dependency graph, +so each test executable can link compiled dependencies without repeating their +link-time code generation. Other release settings are unchanged. + +Build shared libraries needed by tests with the same profile: + +```bash +cargo build --profile ci-test -p deq-decoder-reference-plugin -p qodec-c +cargo test --profile ci-test --workspace --exclude deq-runtime --all-features +``` + +Published wheels still use `--release`. The profile does not change ordinary +local builds, benchmarks, or qodec's development-profile verification runner. + +## Native Wheel Build Tools + +Native CI and release jobs use the versions in +[requirements-build.txt](requirements-build.txt): maturin 1.15.0 and, on Linux, +Zig 0.12.1. From the repository root, install them into your selected environment: + +```bash +python -m pip install -r requirements-build.txt +python -m maturin --version +``` + +Maturin builds Python wheels from the Rust crates. Zig is a compiler toolchain; +here it supplies the C compiler and linker for building Linux wheels against an +older glibc, rather than requiring the build machine's newer glibc at runtime. + +For Linux `maturin build --zig` commands, activate that environment and set: + +```bash +export CARGO_ZIGBUILD_PYTHON_PATH="$PWD/tools/zig.py" +``` + +The [Zig adapter](tools/zig.py) accepts cargo-zigbuild's `-m ziglang` invocation +and removes only `-Wl,-O1`, a linker optimization hint that Zig ignores. Rust +optimization flags and other linker diagnostics are unchanged. Use a fresh Cargo +target directory when checking a toolchain change; Cargo can replay warnings +from an older cached build. + +If maturin's executable version disagrees with `importlib.metadata.version("maturin")`, +reinstall with `python -m pip install --force-reinstall -r requirements-build.txt`. +Run pip through the selected interpreter rather than another environment's pip. +These pins do not change the separate Pyodide toolchain or SBOM policy. + ## Pull Request Process 1. Fork the repository and create your branch from `main` diff --git a/Cargo.toml b/Cargo.toml index ce001e2b..03398409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ members = [ "deq/deqagram", "deq/deqagram/bindings/python", "qodec", + "qodec/bindings/python", + "qodec/bindings/c", ] default-members = [ "binar", @@ -26,6 +28,10 @@ resolver = "2" lto = true codegen-units = 1 +[profile.ci-test] +inherits = "release" +lto = "off" + # Profile for cargo asm: same optimization as release, but without LTO so the # assembly output reflects actual codegen (with LTO, vectorization is deferred # to link time, making cargo asm output misleadingly scalar). diff --git a/README.md b/README.md index 5e541c8c..2986c86c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This repository contains several interconnected crates: - [pauliverse](pauliverse): Fast stabilizer simulators. - [deq](deq): A dynamic and generic QEC decoding system, including the `.deq` DSL, transpiler, JIT runtime (Rust), CLI, and an anywidget-based visualizer. - [deqagram](deq/deqagram): A pest-based parser and typed AST for the `.deq` format (the parser behind deq). +- [qodec](qodec): A shareable model for error-correction protocols. ### Python Bindings @@ -24,6 +25,13 @@ Python bindings are available for several crates: - [paulimer](paulimer/bindings/python): Python bindings for the paulimer and pauliverse crates. - [deqagram](deq/deqagram/bindings/python): Python bindings for the deqagram `.deq` parser. - [deq](deq/deq) and [deq-runtime](deq/deq_runtime): pure-Python frontend and PyO3-based runtime extension. +- [qodec](qodec/bindings/python): Python bindings for authoring and inspecting qodec protocols. Requires Python 3.11 or newer and includes a native Rust extension. + +### C Bindings + +[qodec](qodec/bindings/c) exposes a read-only C ABI for loading and inspecting +protocols. The binding guide covers library builds, the generated header, and +platform verification limits. ## Building @@ -32,23 +40,27 @@ Python bindings are available for several crates: To build this repository, you need: - [Rust](https://www.rust-lang.org/tools/install) (stable toolchain) -- [Python](https://python.org/) (3.9 or later) +- [Python](https://python.org/) (3.9 or later; qodec requires 3.11 or later) - [maturin](https://github.com/PyO3/maturin) (for building Python bindings) ### Building the Rust Crates -To build all crates: +To build the default Rust crates: ```bash cargo build --release ``` -To run tests: +To test the default Rust crates: ```bash cargo test ``` +For qodec's Rust, C, and Python checks, use its +[development guide](qodec/CONTRIBUTING.md). These checks also need `cbindgen`; +the runner builds the C library before testing its compiled caller. + ### Building Python Bindings To build and install the Python bindings for development: @@ -71,6 +83,16 @@ cd deq/deqagram/bindings/python maturin develop --release ``` +For qodec, start at the repository root with Python 3.11 or newer: +```bash +cd qodec/bindings/python +maturin develop --release --extras parsers +``` + +The optional `parsers` extra installs Stim for reading Stim circuits. Omit it +when working only with YAML or a custom parser. See the +[qodec walkthrough](qodec/docs/walkthrough.md) for loading and editing a protocol. + ## Installation ### Rust (from crates.io) diff --git a/qodec/.gitignore b/qodec/.gitignore new file mode 100644 index 00000000..a98ea1e1 --- /dev/null +++ b/qodec/.gitignore @@ -0,0 +1,14 @@ +# Rust +target/ +*.so + +# Python +__pycache__/ +*.pyc +.mypy_cache/ +.pytest_cache/ +bindings/python/docs/autoapi/ +bindings/python/docs/_build/ + +# Coverage artifacts (tools/binding-coverage.sh, cargo llvm-cov) +*.profraw diff --git a/qodec/CHANGELOG.md b/qodec/CHANGELOG.md new file mode 100644 index 00000000..e3680937 --- /dev/null +++ b/qodec/CHANGELOG.md @@ -0,0 +1,121 @@ +# Changelog + +## Unreleased + +### Validation tightened + +These reject documents no consumer could interpret, so `schema_version` is +unchanged under the compatibility contract below. + +- A slice selector may select at most 1048576 positions. Every consumer that + expands a selector allocates one reference per position, so an unbounded + slice such as `circuit.readouts[0:18446744073709551615]` exhausted memory in + the C projection, Python's `expand()`, and `Reference::parse_many`. +- `frames` keys are parsed with the reference grammar, like every equation term. + A misspelled target such as `ou[0].z[0]` now fails to load instead of round-tripping. +- A Pauli token may not carry an operand prefix. `target.Z_0` is rejected; code + qubits are addressed directly, as in `Z_0`. +- Unknown action-step fields and unknown fields inside a rotation are rejected + instead of being silently discarded during serialization. + +### Fixed + +- Removing a layer's gadgets no longer restores old code definitions on save. + Slices retain code bindings for their retained layers, including unused codes. +- Rust slices preserve metadata, explicit schema version, and manifest filename, + matching Python. Source locations and stored artifact maps are not copied. +- C loading rejects strings containing NUL instead of deleting bytes, preventing + name collisions and altered source text. JSON-escaped metadata remains lossless. +- C metadata fields always contain JSON object text, including `{}` when empty. +- Schemas accept the loader's empty action drafts, reference whitespace, and + instruction-set filenames containing `#`. Code Pauli token spelling agrees + with loading; numeric limits and slice arithmetic remain parser checks. +- Directory saves retain unchanged references to files outside the original + manifest's directory. Edits are copied locally; external files are never + overwritten. Reused files are checked for changes before writing. Bundles + copy current values without reusing external files. Generated names cannot + redirect output paths. A manifest filename deliberately pointing above the + destination still raises the output root. +- The loader reports a conflicting artifact kind and a directory path as typed + errors rather than as `io::Error`. +- `Display` no longer panics on a draft it cannot serialize, and no longer + presents a code name where a block type belongs. +- The C header's `logical_count` and `observe_count` describe what a draft can + actually contain. Circuit-qubit identifiers are `uint64_t` everywhere and + counts are `size_t`. + +### API Changes + +- Rust and Python layers expose `codes`, a sparse map from block type to code + definition. Python accepts it as a keyword-only constructor argument. + `Qodec.codes` includes explicitly bound codes without gadgets. Rust `Layer` + literals must supply the new field; an empty map lets encodings supply bindings. +- `Circuit.qubits` is renamed to `Circuit.blocks` in Rust and Python; Rust's + `qubits_with` is renamed to `blocks_with`. The result is still distinct circuit + block labels in first-appearance order, not flattened physical qubits. Python + retains a property. The old names are not aliases. +- Rust `Qodec::save` and `save_bundle` return the written manifest `PathBuf`. + Python `Qodec.save` returns `pathlib.Path`. Pass the returned path to `load`; + destination directories, source sidecars, and relative-path behavior are unchanged. + +## 0.1.0 - Initial Release + +The first release uses package version `0.1.0`, on-disk `schema_version: 1`, +and C ABI revision `1`. These versions are independent. + +### Model + +- Codes, instruction sets, and gadgets describe a lowering chain from logical + instructions to physical operations. Instruction sets declare sibling + `blocks` and `instructions`; circuits contain instruction calls. +- Instructions describe stabilizations, observations, Clifford operations, + rotations, conditions, and declared parameters. Gadgets supply circuits, + boundary encodings, parameter bindings, checks, readouts, and sparse frames. +- Parity equations contain property-path references and integer bits `0` or `1`. + References support indices, slices, and unions. Frames describe additional + output-sign corrections; omission does not reset incoming frames. + +### Persistence and Interpretation + +- Load and save referenced YAML or JSON artifacts and multi-document YAML + bundles. Reference fields determine artifact types; filenames are unrestricted. + Saves serialize the current model, including edits and metadata. +- Loading and saving preserve incomplete protocol drafts and uninterpreted + circuit source. They enforce preservation preconditions, not protocol + correctness. Protocol checks belong to `qdk.ec.audit`. +- YAML circuit parsing is built in. Rust and Python register source parsers in + one Rust-owned registry; the latest registration wins. Python's optional + `qodec[parsers]` extra supplies a Stim adapter with bounded repeat expansion + and noise-free constant readouts. Parsing is separate from persistence. + +### Language APIs + +- Rust and Python support construction, editing, structural equality, model + navigation, source locations, and persistence. +- Python requires 3.11 or newer and includes type stubs. Its native extension + uses CPython's stable ABI starting at Python 3.11. +- C provides a read-only projection with explicit ownership, error reporting, + and an ABI-version check. C bindings are distributed as source. +- JSON Schemas, language guides, and example protocols describe the format and + its use. Example audit limits are listed in [examples/README.md](examples/README.md). + +## Compatibility Contract + +qodec is pre-1.0. Each `0.x` minor release may break the public API or on-disk +format; patch releases preserve compatibility. Pin an exact package version +for reproducible work. + +- `schema_version` is an optional non-negative integer on the manifest. When + present, it must equal the loader's `CURRENT_SCHEMA_VERSION`. Omission means + the loader's current version; declare it explicitly in persistent datasets. +- Increment `schema_version` by exactly one when a previously valid artifact + would fail to load, change meaning, need a new required field, or have its + references or bundle entries interpreted differently. Adding optional fields + or rejecting already-invalid documents does not require an increment. +- There is no compatibility window for explicit schema versions. Changing the + number alone does not migrate a document; its contents must match the target + format. A package release may leave the schema version unchanged. +- The C ABI revision is independent of the schema and package versions. Bump it + for a breaking change to a C symbol, signature, struct layout, or calling + convention. Callers must compare `qodec_abi_version()` with the header's + `QODEC_ABI_VERSION` before accessing projected structs. \ No newline at end of file diff --git a/qodec/CONTRIBUTING.md b/qodec/CONTRIBUTING.md new file mode 100644 index 00000000..8b0240c7 --- /dev/null +++ b/qodec/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contributing to qodec + +qodec is in early development (preview). Schemas and APIs are still evolving; +each pre-1.0 minor release may introduce breaking changes. + +Follow the parent [contribution guide](../CONTRIBUTING.md) for issues, pull +requests, the contributor license agreement, and the code of conduct. +The parent [support policy](../SUPPORT.md) applies. For qodec issues, include +the relevant YAML snippets or error messages in addition to reproduction steps. + +## Building and Testing + +qodec has a Rust model, Python bindings, and a read-only C ABI. Run the following +from the **qodec directory**, the one containing `examples/` and `tools/`. Use +the Python environment selected for your work. + +```bash +python tools/check.py all --dry-run +python tools/check.py all +``` + +The [verification runner](tools/README.md) covers Rust/C, Python types and tests, +documentation, coverage, and example audits. The example scope requires a +compatible development QDK with `ec` dependencies. It is stricter than the +current source CI, which does not yet install that audit engine. Do not count +CI's binding tests as evidence of example audits. + +For a focused change, choose a runner scope (`rust`, `python`, `docs`, +`coverage`, `examples`, or `packaging`). See [prerequisites](../.github/instructions/qodec-checks.instructions.md#environment) +for compiler, header-generation, and coverage tools. A focused core test can use +`cargo test -p qodec --test round_trip_test`. + +## Documentation + +Model concepts and the YAML format live in [docs/](docs/README.md). Language +guidance lives with the corresponding API: crate-level rustdoc in +[src/lib.rs](src/lib.rs), Python usage and stub-generated reference in +[bindings/python/docs/](bindings/python/docs/index.rst), and the +[C binding guide](bindings/c/README.md) with its generated header. + +Build locally from the qodec directory, using Python 3.11 or newer with the +Python binding installed for its examples: + +```bash +RUSTDOCFLAGS="-D warnings" cargo doc -p qodec -p qodec-python -p qodec-c --no-deps +cargo test -p qodec --doc +python -m pip install -r bindings/python/docs/requirements.txt +python -m sphinx -W --keep-going -b html bindings/python/docs target/python-docs/html +python -m sphinx -W --keep-going -b doctest bindings/python/docs target/python-docs/doctest +python bindings/python/docs/test_docs.py +``` + +Open `../target/doc/qodec/index.html` for Rust or +`target/python-docs/html/index.html` for Python. The C reference is +[bindings/c/include/qodec.h](bindings/c/include/qodec.h); regenerate it from the +Rust binding source as described in the C guide. + +Python API pages read the `.pyi` signatures and docstrings without importing the +native extension. PyO3 constructors are documented as `__new__` methods. Keep +runtime docstrings useful for `help()`, and run mypy and stubtest after stub edits. +CI builds both documentation sets with warnings as errors and runs the Python +examples and generated-reference checks on Python 3.12. Generated files are not +checked in. + +## Code Standards + +Rust: + +- Code must pass `python tools/check.py rust`, which formats, lints, and tests + `qodec`, `qodec-python`, and `qodec-c` without including sibling packages. + Clippy uses the parent's `--all-targets --all-features -- -D clippy::pedantic` + policy. The C library is built before its tests. +- New features should include tests +- Public APIs should be documented + +Python bindings: + +- Expose a Pythonic API +- Keep the `.pyi` stub files in sync with the extension (run `mypy`) +- Add tests under `bindings/python/tests/` + +On-disk format changes: + +- Any breaking change to an artifact format must bump `schema_version` and be + recorded in [`CHANGELOG.md`](CHANGELOG.md). See that file for the + compatibility contract. + +## Releasing + +See [RELEASING.md](RELEASING.md) for versioning and release verification. diff --git a/qodec/Cargo.toml b/qodec/Cargo.toml index fbbc8b5d..862ba714 100644 --- a/qodec/Cargo.toml +++ b/qodec/Cargo.toml @@ -1,12 +1,34 @@ [package] name = "qodec" -version = "0.0.0" +version = "0.1.0" edition = "2024" -authors = ["Microsoft Corporation"] -description = "Reserved name for an upcoming Microsoft QDK error-correction crate. Not yet implemented." license = "MIT" +authors = ["Microsoft Corporation"] +description = "qodec — a formal model of quantum error correction protocols" +readme = "README.md" +exclude = [".vscode/**"] repository = "https://github.com/microsoft/qdk-ec" homepage = "https://github.com/microsoft/qdk-ec" -readme = "README.md" +keywords = ["quantum", "qec", "error-correction", "stabilizer", "codec"] +categories = ["science", "encoding"] +publish = false + +[package.metadata.release] +release = true +shared-version = "qodec" +tag = false [dependencies] +derive_more = { version = "2.1.1", features = ["from", "display", "as_ref"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" +unsafe-libyaml = "0.2" +yaml-rust2 = "0.11" + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tempfile = "3.27.0" +jsonschema = { version = "0.29", default-features = false } +proptest = "1.0" diff --git a/qodec/LICENSE b/qodec/LICENSE new file mode 100644 index 00000000..22aed37e --- /dev/null +++ b/qodec/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/qodec/README.md b/qodec/README.md index b1ced9e6..317c1b9d 100644 --- a/qodec/README.md +++ b/qodec/README.md @@ -1,6 +1,259 @@ # qodec -Reserved name for an upcoming Microsoft QDK error-correction crate. Not yet implemented. +> **A formal model of quantum error correction protocols.** -This crate name is reserved by Microsoft for an upcoming component of the -[QDK error-correction project](https://github.com/microsoft/qdk-ec). +QEC tools provide codes, circuits, simulators, and decoders. What is missing +is a common description of **how those parts fit together**: which logical +instruction a circuit implements, how its qubits are encoded, and what its +measurements mean. These connections often live ad-hoc in papers, generator scripts, +and conventions, leaving each tool to reconstruct them. + +qodec records instruction sets, codes, and gadgets in one formal description. +You choose the logical and physical instruction sets. Each instruction's +meaning is defined formally, so a tool can check that a gadget realizes the +instruction it claims to, and a protocol written against one instruction set can +be lowered onto a different physical one without rewriting what it means. + +A qodec is a sequence of layers. At each layer, gadgets implement a source ISA (instruction set architecture) +using operations from a target ISA: + +```mermaid +flowchart LR + L(["`**Source ISA**`"]) --> G["`**Gadgets**`"] --> P(["`**Target ISA**`"]) + classDef isa fill:#eff6ff,stroke:#60a5fa,color:#1e3a8a,stroke-width:1.25px + classDef gadget fill:#fff1f2,stroke:#fda4af,color:#9f1239,stroke-width:1.25px + class L,P isa + class G gadget +``` + +Usually, the source ISA is logical, the target ISA is physical, and the gadgets +describe fault-tolerant circuits. + +Use qodec to share a protocol, verify its circuits, compare implementations, +and reuse it across simulation and decoding experiments. Start with one +instruction and its gadget; a complete architecture is not required. +See [Using qodec with QDK, deq, and Stim](docs/ecosystem.md) for the connections +between these tools. + +## Example + +The smallest working qodec in this repository is [`repetition3.qodec.yaml`](examples/repetition3/repetition3.qodec.yaml), the three-qubit bit-flip repetition code lowered to a stim-compatible "physical" layer. + +### Layers + +A qodec is a stack of abstraction layers, each layer pairs an instruction set architecture (ISA) with gadgets that lower it to the next. Here there are two layers, a `repetition3` logical layer and a `stim+rz` physical layer. Any number of layers is possible in general. + +```yaml +name: repetition3 +layers: + - instruction_set: repetition3.isa.yaml + codes: + repetition3: repetition3.code.yaml + gadgets: + prepare_z: prepare_z.gadget.yaml + idle: idle.gadget.yaml + measure_z: measure_z.gadget.yaml + rotate_z: rotate_z.gadget.yaml + - instruction_set: stim+rz.isa.yaml +``` + +### Instructions + +The repetition3 ISA declares the user-visible instructions: `prepare_z`, `idle`, `measure_z`, and a non-Clifford `rotate_z`. Each instruction defines its operands and parameters _and_ its formal action: + +```yaml +- mnemonic: measure_z + description: Destructive Z-basis measurement of the logical Z observable. + in: [repetition3] + action: [observe: Z_0] +``` + +### Gadgets + +A gadget lowers one such instruction to the next layer. It provides a `circuit` that realizes the instruction, parity `checks` that XOR to zero (unless there are errors), and `readouts` that relate the instruction's outcomes to circuit measurements. A decoding tool, e.g. [deq](https://github.com/microsoft/qdk-ec/tree/main/deq), combines these relationships with a fault model. The gadget below implements the `measure_z` instruction from above. + +```yaml +circuit: {format: stim, source: "M 0 1 2"} +checks: + - ["circuit.readouts[0]", "circuit.readouts[1]", "in[0].stabilizers[0]"] + - ["circuit.readouts[1]", "circuit.readouts[2]", "in[0].stabilizers[1]"] +readouts: [["circuit.readouts[0]", "in[0].z[0]"]] +``` + +Here `in[0]` refers to the input encoding, the instruction's first input block. In this case, the repetition3 code on physical qubits 0–2. + +The [walkthrough](docs/walkthrough.md) traces the same example end to end. See the [examples/](examples/) folder for more. + +## Features + +- **Choose your physical ISA.** Native gates, rotations, and measurements; no fixed gate set. +- **Universal, not just Clifford.** Include Toffoli and parameterized Pauli rotations. +- **Separate meaning from implementation.** Verify circuits against declared actions, not gate names. +- **Mix, switch, and concatenate codes.** Keep every encoding level explicit. +- **Bring your own circuits.** Retain your existing [source formats](docs/representations/source-formats.md); analysis needs a compatible parser. +- **Keep measurement meanings.** Checks and readouts travel with the circuit, independently of fault models and decoders. +- **Check structure and correctness separately.** [Schemas](schemas/) define file structure; [audit](docs/concepts/validation.md) checks supported circuits and actions. + +## Artifacts + +A qodec contains the following artifact types. + +**Top-level artifacts**, the building blocks of a qodec: + +| Artifact | Role | +| -------------------------------------------- | ----------------------------------------------------------------------------------- | +| [Instruction Set](docs/concepts/instruction-set.md) | Defines available operations per layer | +| [Gadget](docs/concepts/gadget.md) | Per-instruction lowering rule: implements, circuit, encodings, checks, readouts, and frames | +| [Code](docs/concepts/code.md) | Stabilizers, logicals | + +**Parity checks and readouts** are normally inlined inside a gadget, but can be factored out into their own file and referenced by relative path (useful for large generated lists). A circuit is always inline; what it may reference is its source text: + +| Component | Role | +| ----------------------------------------------------- | ---------------------------------------------------------- | +| [Circuit](docs/concepts/gadget.md#circuit-object) | Realization circuit/program source | +| [Parity checks](docs/concepts/gadget.md#checks) | Deterministic-check list | +| [Readouts](docs/concepts/gadget.md#readouts) | Output-bit list: observables then flags (parity equations) | + +### Instruction Sets + +The manifest's `layers` field orders instruction set architectures (**ISA**s) from logical to physical. That order is the lowering chain. Each ISA declares instructions that operate on blocks, named groups of qubits such as a `[[4,2,2]]` code block or a single physical qubit. + +### Gadgets + +A **gadget** is a per-instruction lowering rule. Each gadget pairs one source-ISA instruction (the one it implements) with a target-ISA realization (the circuit, plus its boundary encodings) plus the decoding surface needed to reason about it under noise: parity checks and readouts. + +**Checks** are deterministic combinations of measurement outcomes (and optionally stabilizers from the input/output encodings) whose parity is zero in the absence of errors. **Readouts** are the bits the gadget exposes. Its logical observables come first, each a combination of measurement outcomes that produces a logical output, followed by any flags (heralding bits the decoder passes through). + +### Codes + +In the repetition-code example, three physical qubits store one logical qubit. +Its **code definition** lists the stabilizers that define valid encoded states +and the physical operators that represent logical X and Z. + +A layer chooses a code for each kind of block. Each gadget specifies which +blocks at the layer below carry each encoded block at its input and output. You can +change the code and its gadgets without changing what the instruction means. + +## Representations + +A qodec is an **abstract object, not a file format**: the forms below represent the same underlying qodec. The Rust model is the source of truth for the schemas and bindings. + +- **In-memory domain types**: the [Rust crate](src/) provides a typed value for every artifact, with serde support throughout. The [Python package](bindings/python/) is a thin PyO3 layer over it, exposing the same data model. See [`bindings/python/`](bindings/python/) for the full API and worked examples. +- **C ABI**: [`bindings/c/`](bindings/c/) provides a read-only C interface. It is distributed as source; build a shared or static library and use the provided [`qodec.h`](bindings/c/include/qodec.h) header. +- **Referenced YAML files**: a manifest and its artifacts, linked by relative path, with no required directory layout or filename suffix. Diff-friendly and hand-authorable. +- **Single-file YAML bundle**: one multi-document YAML stream whose first document contains the manifest in a single-entry envelope with an unrestricted key. Remaining entries are looked up by referenced path. + +## Installation + +### Python + +Python 3.11 or newer is required. Follow the +[source build instructions](bindings/python/README-python.md#build-and-install-from-source) +to install from a qdk-ec checkout. + +Use `--extras parsers` with `maturin develop` to install optional external +parsers, currently Stim. YAML interpretation is built in, and the base package can load and save +Stim source without the extra. See +[parser registration](docs/representations/source-formats.md#registering-a-parser) +for other languages and custom parsers. + +The example files are not installed with the Python package. Run the following +from the qodec source directory containing [examples/](examples/). Outside the checkout, +obtain [the repetition3 bundle](examples/repetition3/repetition3.qodec.yaml) +and replace the path passed to `load` with its location. + +```python +import qodec + +protocol = qodec.Qodec.load("examples/repetition3/repetition3.qodec.yaml") +print(protocol) + +gadget = protocol.layers[0].gadgets["measure_z"] +print(gadget.circuit.source) +``` + +See the [Python guide](bindings/python/docs/usage.rst) for creating, editing, +and saving protocols. + +## Documentation + +Start with the [walkthrough](docs/walkthrough.md) to follow a logical instruction +through its implementation. The remaining docs describe the +[model](docs/concepts/model.md) and its +[representations](docs/representations/yaml.md). + +| Document | Content | +| ---------------------------------------------- | ---------------------------------------------------- | +| [Walkthrough](docs/walkthrough.md) | Worked example: one instruction from declaration to physical execution | +| [The qodec model](docs/concepts/model.md) | The abstract object: lowering chain, artifact kinds, gadgets versus compilation | +| [Instruction Set](docs/concepts/instruction-set.md) | ISA: blocks, instructions, actions | +| [Code](docs/concepts/code.md) | Stabilizers and logical operators | +| [Gadget](docs/concepts/gadget.md) | Instruction implementation, encodings, checks, readouts, and frames | +| [Validation](docs/concepts/validation.md) | What loading preserves and what audit checks | +| [Using qodec with QDK, deq, and Stim](docs/ecosystem.md) | Existing connections, shared workflows, and future compatibility | +| [YAML representation](docs/representations/yaml.md) | On-disk forms, loading, source formats, shorthands | +| [Python API](bindings/python/README-python.md) | Usage guide and stub-generated API reference | +| [Rust API](src/lib.rs) | Crate-level guide and rustdoc reference | +| [C API](bindings/c/README.md) | Build/linking guide, ownership, and generated header | + +See [Building the documentation](CONTRIBUTING.md#documentation) for local HTML +builds of the Rust and Python API reference. + +## FAQ + +**Where does the name come from?** +From [codec](https://en.wikipedia.org/wiki/Codec), which encodes and decodes +digital information. A qodec describes the quantum counterpart: encoding +logical qubits into physical qubits and decoding physical measurements into +logical results. + +**Why not use deq?** +Use [deq](https://github.com/microsoft/qdk-ec/tree/main/deq) to model faults and +run decoding experiments. Both tools describe codes and gadgets. qodec also +specifies what each instruction must do, separately from its circuit, and lets +you choose the physical instructions and encoding levels. You can compare +circuits against that specification while changing fault models or decoders. +Using both tools requires [gadget translation and a fault model](docs/ecosystem.md#deq-the-decoding-surface). + +**Why not use Stim?** +Use Stim to simulate and analyze stabilizer circuits. qodec records which +logical instruction a circuit implements, how its qubits are encoded, and what +its measurements mean. That lets you compare or replace circuits without +reconstructing their purpose. The Stim circuit remains the gadget's source. + +**Why not use QASM or QIR?** +They describe a program to run. qodec adds a separate specification of the +logical instruction, how its qubits are encoded, and what its measurements +mean. The program remains the gadget's circuit source. + +**Why YAML?** +YAML is easy to read, edit, and compare in version control. Tools in different +languages can read the same files. Files are optional: use the Rust or Python +API to build qodec objects in memory. The C API is read-only. + +**Does qodec do synthesis, compilation, or decoding?** +No. qodec defines the protocol; other tools compile it, simulate it, decode +measurements, or estimate resources. See [the model guide](docs/concepts/model.md#the-lowering-chain) +for how those tools use qodec. + +**Is qodec stable?** +Not yet. Any pre-1.0 minor release may change the API or file format. Pin an +exact package version for reproducible work. The manifest's `schema_version` +tracks file-format changes separately from the package version. Loading +requires a matching schema version; if omitted, the loader assumes its current +version. See [CHANGELOG.md](CHANGELOG.md) for the compatibility rules. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for qodec's development workflow and required checks. +The parent repository owns the [code of conduct](../CODE_OF_CONDUCT.md), +[security reporting policy](../SECURITY.md), and [support policy](../SUPPORT.md). +Do not report security vulnerabilities through public issues. + +## License + +Licensed under the MIT License. See [`LICENSE`](LICENSE). + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/qodec/RELEASING.md b/qodec/RELEASING.md new file mode 100644 index 00000000..15d6d05e --- /dev/null +++ b/qodec/RELEASING.md @@ -0,0 +1,83 @@ +# Releasing qodec + +## Version Ownership + +Each qodec crate declares its own `[package].version` in +[Cargo.toml](Cargo.toml), [bindings/python/Cargo.toml](bindings/python/Cargo.toml), +or [bindings/c/Cargo.toml](bindings/c/Cargo.toml). + +- The three crates use `shared-version = "qodec"` in their + `[package.metadata.release]` tables. This named group keeps their versions + aligned without coupling other qdk-ec packages. +- The Python package declares a dynamic version in + [pyproject.toml](bindings/python/pyproject.toml); maturin reads the binding + crate's version. +- `qodec.__version__` comes from installed package metadata. A checkout without + installed metadata reports `0+unknown`. + +Use the scoped `cargo release version` commands below instead of editing each +version manually. The packaging tests check that the three versions agree. +Schema and ABI revisions are independent of the package version: + +| Version | Source | +| --- | --- | +| Rust package | [Cargo.toml](Cargo.toml) | +| Python package | [bindings/python/Cargo.toml](bindings/python/Cargo.toml) | +| C binding crate | [bindings/c/Cargo.toml](bindings/c/Cargo.toml) | +| On-disk schema | [CURRENT_SCHEMA_VERSION](src/manifest.rs) | +| C ABI | [QODEC_ABI_VERSION](bindings/c/src/lib.rs) | + +Follow the [compatibility contract](CHANGELOG.md#compatibility-contract) when +changing a schema or ABI revision. Update affected examples, fixtures, and +documentation, and regenerate the C header after an ABI change. From the qdk-ec +repository root: + +```bash +bash qodec/bindings/c/regenerate.sh +``` + +## Bump Versions + +qodec is pre-1.0: each `0.x` minor may break the API or on-disk format. Use a +patch release only for compatible changes. Consumers should pin an exact version. + +Install the version driver with `cargo install cargo-release --locked`. From +the qdk-ec repository root, preview the next bump before executing it: + +```bash +cargo release version minor -p qodec -p qodec-python -p qodec-c +cargo release version minor -p qodec -p qodec-python -p qodec-c --execute +``` + +The first command is a dry run. The second updates the selected package versions +and any affected lockfile entries. Neither command commits, tags, pushes, or +publishes. Start from a clean worktree before using `--execute`, review the diff, +and commit the changes through the normal pull-request process. A `patch` bump +or an explicit version can be supplied instead. Update the changelog with the +release's user-visible changes and applicable schema and ABI revisions. + +For prerelease versions, replace `minor` with `alpha` in the same scoped command +to produce `X.Y.Z-alpha.N`, or with `release` to drop the suffix. Do not use +`patch`, `minor`, or `major` to advance an alpha: those remove the suffix and +advance the base version. +Maturin maps Rust prerelease suffixes to PEP 440 wheel versions, such as +`-alpha.1` to `a1`. + +## Verification and Publication + +From the qdk-ec repository root, run the +[verification gates](../.github/instructions/qodec-checks.instructions.md) with +the selected Python environment: + +```bash +python qodec/tools/check.py all +``` + +The gates cover Rust, C, Python, documentation, coverage, example audits, and +source-distribution rebuilding. Example audits require a compatible QDK build +with its `ec` dependencies. Check the release artifacts outside the source +checkout and require CI to pass for the release commit. + +Publication uses the parent [release pipeline](../.ado/publish.yaml), not the +version-bump command. Keep the binding crates unpublished as Rust packages. +Create release tags only on reviewed, merged commits. \ No newline at end of file diff --git a/qodec/bindings/c/Cargo.toml b/qodec/bindings/c/Cargo.toml new file mode 100644 index 00000000..f7004a38 --- /dev/null +++ b/qodec/bindings/c/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "qodec-c" +version = "0.1.0" +edition = "2024" +description = "C bindings for qodec" +license = "MIT" +authors = ["Microsoft Corporation"] +publish = false + +[package.metadata.release] +release = true +shared-version = "qodec" +tag = false + +[lib] +name = "qodec_c" +# cdylib for dlopen/dynamic linking, staticlib for static linking, rlib so the +# Rust-side tests can call the shims directly. +crate-type = ["cdylib", "staticlib", "rlib"] + +[dependencies] +qodec = { path = "../.." } +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.27.0" diff --git a/qodec/bindings/c/README.md b/qodec/bindings/c/README.md new file mode 100644 index 00000000..406b4f3c --- /dev/null +++ b/qodec/bindings/c/README.md @@ -0,0 +1,317 @@ +# qodec: C bindings + +The C bindings expose a read-only projection of the +[qodec model](../../docs/concepts/model.md) through a C ABI. Decoders, samplers +and visualizers can read its layers, instructions, gadgets and codes without +embedding a Rust or Python language runtime. Authoring and mutation use the +Rust or Python API instead. + +Loading builds the projection and returns its `Qodec` root. From there, reading +is plain struct traversal. There are five functions; the generated +[include/qodec.h](include/qodec.h) is the canonical reference for their +signatures, constants, struct fields and safety contracts. + +## Building and linking + +Run from the qodec directory, the one containing `examples/` and `tools/`: + +```bash +cargo build -p qodec-c --release +``` + +Produces both linkable forms in the qdk-ec workspace's `target/release/` +(`../target/release/` from the qodec directory), unless `CARGO_TARGET_DIR` +selects another location: + +| Artifact | Use | +| --- | --- | +| `libqodec_c.so` / `.dylib` / `qodec_c.dll` | dynamic linking, `dlopen` | +| `libqodec_c.a` / `qodec_c.lib` | static linking | + +On Linux, build and run the tested [examples/demo.c](examples/demo.c) with static +linking from the same qodec directory. Adjust the library path if you use a +custom target directory: + +```bash +cc -std=c11 -pedantic -Wall -Wextra -Werror -I bindings/c/include \ + bindings/c/examples/demo.c ../target/release/libqodec_c.a \ + -lpthread -ldl -lm -o /tmp/qodec-demo +/tmp/qodec-demo examples/repetition3/repetition3.qodec.yaml +``` + +For dynamic linking on Linux, replace the archive with +`-L ../target/release -lqodec_c` and make `../target/release` available to the runtime +loader, for example through `LD_LIBRARY_PATH`. Native system libraries and +loader settings differ by platform. + +The source CI runs the C compiler smoke test on Linux. Its current compiler +flags and system libraries are Unix-style; passing it does not qualify the +Windows/MSVC build or the other release targets. + +The checked-in header is generated by cbindgen and **must not be edited by +hand**. Binding changes belong in [src/lib.rs](src/lib.rs); regenerate with +[regenerate.sh](regenerate.sh). `header_is_in_sync_with_the_source` checks that +the header matches the source. + +## Using it + +`qodec_load` accepts a path to a manifest file or a single-file bundle only. +The file may have any name. References select artifact types, not filename +suffixes or directory layout; only referenced documents are loaded. +Unreferenced files and bundle entries are ignored, and directories are not +scanned. Each manifest `gadgets` value must reference a YAML gadget document, +including circuit-only wrappers such as `circuit: ./idle.stim`, not raw Stim +source. Circuit-source extensions and inline `format` still identify the +source language. + +Loading checks preservation preconditions, not protocol correctness. Incomplete +declarations and invalid circuit text can load through the C API too. The source +is retained and circuit parse errors appear on the circuit projection. Consumers +must check their calculation's preconditions; `qdk.ec.audit` provides protocol +checks through Python. See [validation](../../docs/concepts/validation.md). + +The first bundle document is a single-entry `{key: manifest}` mapping with +an arbitrary key. Remaining entries are looked up by normalized reference +paths, preserving leading `..` components. Referenced circuit sources may +be bundle entries or separate files on disk; circuit, check, and readout +paths are relative to their gadget document. An explicit `schema_version` +must be 1; other versions are rejected. The C ABI is 1. + +Directory paths return `QODEC_STATUS_ERROR`, even when they contain a manifest; +`qodec_last_error` reports "expected a manifest file path". The path must be a +NUL-terminated UTF-8 string, and the output pointer must be non-null and +writable. This complete program runs from the qodec directory: + +```c +#include + +#include "qodec.h" + +int main(void) { + if (qodec_abi_version() != QODEC_ABI_VERSION) { + fprintf(stderr, "qodec ABI mismatch\n"); + return 1; + } + + Qodec *qodec = NULL; + if (qodec_load("examples/repetition3/repetition3.qodec.yaml", &qodec) != QODEC_STATUS_OK) { + const char *message = qodec_last_error(); + fprintf(stderr, "%s\n", message != NULL ? message : "qodec load failed"); + return 1; + } + + for (size_t layer_index = 0; layer_index < qodec->layers.count; ++layer_index) { + const QodecLayer *layer = &qodec->layers.items[layer_index]; + printf("%s: %zu gadgets\n", layer->instruction_set_name, layer->gadgets.count); + + for (size_t gadget_index = 0; gadget_index < layer->gadgets.count; ++gadget_index) { + const QodecGadget *gadget = &layer->gadgets.items[gadget_index]; + printf(" %s\n", gadget->implements.mnemonic); + } + } + + qodec_unload(qodec); + return 0; +} +``` + +The demo goes further: it reads actions, circuit calls, encodings and parity +references, and handles a missing gadget. The [Testing](#testing) section +describes how it is compiled and run against the library. + +## Reading the projection + +Layers are ordered from logical to physical. Follow their fields to read +instruction sets and gadgets, then each gadget's circuit and boundary +encodings. `qodec_find_gadget` finds a gadget within a layer by mnemonic; +otherwise use fields and indices directly. + +Ordinary collections are `{ count, items }`: iterate from zero to `count`, +exclusive. Empty collections have null storage pointers; do not dereference +them. Optional strings are `NULL` when absent. Other strings are borrowed, +NUL-terminated UTF-8 `const char *` values. Strings containing NUL cannot cross +this boundary: `qodec_load` returns `QODEC_STATUS_ERROR` and `qodec_last_error` +identifies the rejected value. This includes names, circuit source, parsed +string arguments, and string-list entries. No bytes are removed. Rust, Python, +and on-disk strings remain unrestricted by this C representation limit. + +Metadata fields always contain JSON object text, including `{}` when no metadata +is present. JSON escapes NUL bytes, so metadata can retain them without failing +C projection. + +String lists use a flat byte buffer and offsets instead of `items`. In a +`QodecStrings` value, `bytes + offsets[string_index]` is a NUL-terminated string +for each `string_index < count`. Offsets count bytes, including terminators: +the strings `"ab"` and `"cd"` have offsets `[0, 3, 6]` and `total == 6`. +Do not read either buffer when `count == 0`. + +`QodecAction` and `QodecArgumentValue` are tagged unions: switch on `tag` and +read only the matching member. For example, `QodecAction_Observe` selects +`observe.observables`, and `QodecArgumentValue_Qubit` selects `qubit.index`, +not `integer.value`. + +An instruction declares block operands in `inputs` and `outputs`, and classical +parameters in `parameters`. `QodecParameter.kind` describes a declared parameter +type; `QodecArgumentValue.tag` describes a supplied value's representation. +These are distinct classifications. + +Boolean literals use `QodecArgumentValue_Boolean` and `boolean.value` (C `bool`), +distinct from integer literals 0 and 1. Selection constraints still use integer +bits in `QodecSelectConstraint.bit`. + +At a call site, `QodecInstructionCall.operands` binds blocks, while `arguments` +supplies values to the declared parameters. Both collections contain +`QodecArgument` entries: `name` is `NULL` for a block operand and holds the +parameter name for an argument. + +Circuit operands are block labels: `Qubit` carries a numeric label and `Text` +a named label. A numeric label is not a position in a projected array. +Encoding `support` stores those labels as strings, such as `"7"` or `"ancilla"`. +By contrast, `Readout` argument indices are absolute, zero-based positions in +the circuit's measurement record across preceding calls. + +## Circuits + +`circuit.source` holds the program text. `circuit.calls` contains calls +parsed against the target ISA while building the C projection. YAML is built +in. Native Stim support is deferred: a normal C build preserves Stim source +but reports that no parser is registered for it. Installing Python's +`qodec[parsers]` extra does not change this native library. + +A Rust host calling `qodec-c` through the same linked Rust instance can register +a parser before loading the C projection. A separately loaded shared library +can have an independent registry. The pure C ABI has no callback registration; +other C consumers interpret the retained source with their own tooling. + +A parse failure or an undeclared instruction leaves `circuit.calls` empty and sets +`circuit.error` without failing `qodec_load` or setting `qodec_last_error`. +Check `circuit.error` before using the calls. `format` is the resolved +source-format tag (possibly `NULL`); `effective_format` is the format selected +for parsing, even if no parser is available for it. + +A call's `select` stores flag-matching patterns as flat `constraints` with +offsets. Each pattern matches when all its constraints match; selection matches +any pattern. `count == 0` means no selection constraint. Do not read offsets +when `count == 0` or constraints when `total == 0`. + +Inline YAML calls use `{mnemonic: {operands: [...], arguments: {name: value}, +select: [{flag: 0}]}}`. Each inner field is optional and defaults to empty. +The list shorthand contains block operands followed by named parameter +arguments; it has no special `select` entry. For an instruction named `select` +with an integer parameter and a flag also named `select`: + +```yaml +- select: {operands: [0], arguments: {select: -2}, select: [{select: 0}]} +- select: [0, select: -3] +``` + +Both calls project a named `select` argument independently of selection. +Only the first has a pattern in `QodecInstructionCall.select`. + +## Parity and references + +`QodecParity` stores equations in compressed-sparse-row form: one flat +`references` array and offsets marking each equation's start and end. Equation +`equation_index` uses +`references[offsets[equation_index] .. offsets[equation_index + 1]]`. For a +nonempty collection, there are `count + 1` offsets. Never read offsets when +`count == 0`, or references when `total == 0`. + +Both gadget checks and readouts use this storage. Checks declare zero parity; +readouts define output bits and are not all zero-parity checks. Readouts are +ordered with the instruction's `observe` outcomes first, followed by its flags. +`observe_count` is the split point. `readout_names` contains labels, with an empty +string for each anonymous readout; references use positions, not those labels. + +`QodecGadget.frames` uses the same parity storage. `frame_targets` contains one +output logical-sign path per equation, in sorted key order. Missing entries and +empty equations apply no additional correction. These are correction definitions, +not zero-parity checks; see [frames](../../docs/concepts/gadget.md#frames). + +A `QodecReference` is a flat struct, not a union or an authored string. Its +reference kind is selected by `tag`, using the `QODEC_REFERENCE_*` constants. +For circuit or gadget readouts, `index` is the zero-based position of the bit. +For `QODEC_REFERENCE_ENCODING_PROPERTY`, `boundary` selects the gadget's +`inputs` or `outputs`, `entry` is the zero-based encoding position, `property` +selects stabilizers, logical X or logical Z, and `index` selects the operator +within that list. `boundary` and `property` matter only for encoding properties. + +`QODEC_REFERENCE_CONSTANT` identifies an integer literal bit: `index` is `0` or +`1`, and the other fields are zero. Constants are never measurement indices. + +Selectors arrive expanded: `circuit.readouts[0:6]` contributes six references, +and `circuit.readouts[0,2,5]` contributes three. The shared +[model](../../docs/concepts/model.md) explains +what these artifacts mean; the header defines their C layout. + +## Ownership and lifetimes + +The library allocates the projection in `qodec_load` and releases it in +`qodec_unload`. Treat the root and everything reachable from it as read-only. +Every nested pointer, string and collection borrows from that root and becomes +invalid when it is unloaded, including pointers returned by `qodec_find_gadget`. + +Pass `qodec_unload` exactly the root pointer returned by `qodec_load`, once, +not a copy of the struct. Passing `NULL` is a no-op; unloading twice is +undefined behavior. Never call `free` on the root or any borrowed storage. + +Several threads may read a loaded qodec concurrently. Keep it alive until all +readers have finished; unloading must not race with any read. + +## Errors and validation + +`qodec_load` is the only function returning a status. `QODEC_STATUS_OK` means +success; failures return `QODEC_STATUS_ERROR`, `QODEC_STATUS_INVALID_ARG` or +`QODEC_STATUS_PANIC`. A caught Rust panic during loading becomes a status, +not an unwind into C. Failed loads leave the output pointer unchanged. +Initialize it to `NULL` and use it only after `QODEC_STATUS_OK`. + +`qodec_find_gadget` returns a borrowed pointer on an exact, case-sensitive +mnemonic match. It returns `NULL` for no match, a null argument or a caught +Rust panic. + +After a failing load status or a `NULL` lookup result, call `qodec_last_error` +for the calling thread's message. Its read-only pointer is valid until another +error is recorded on that thread or the thread exits; copy the message to +retain it, and never free it. It is `NULL` if no error has been recorded. +Successful calls do not clear older errors, so check the operation's return +value before reading the message. NUL bytes in diagnostic text are escaped as `\0`. + +Loading uses the Rust model's loader. See the shared +[validation rules](../../docs/concepts/validation.md) for checks and their +limits. A successful load is not proof that every circuit parsed or that a +gadget implements its claimed instruction: inspect `circuit.error` before +using its calls, and leave circuit verification to tools that interpret +the circuit. + +## ABI stability + +Compare `qodec_abi_version()` with the header's `QODEC_ABI_VERSION` before +reading any projected struct. When using `dlopen`, resolve and call it first; +refuse to proceed on a mismatch. Use a header and library with matching ABI +revisions rather than copying the numeric revision into application code. + +The structs are transparent, so **their layout is part of the ABI**. Treat +layout changes as breaking: even appending a field can change array strides or +the layout of an enclosing struct. Breaking changes to layouts, signatures, +symbols or calling conventions require an ABI revision. This revision is +independent of the package version and the on-disk `schema_version`. + +## Testing + +From the repository root, build the static library before running the tests: + +```bash +cargo build -p qodec-c +cargo test -p qodec-c +``` + +[tests/abi_test.rs](tests/abi_test.rs) traverses the projection from Rust. +[tests/c_smoke_test.rs](tests/c_smoke_test.rs) runs on Linux and compiles the demo with +`-std=c11 -pedantic -Wall -Wextra -Werror`, runs it on fixtures and checks the +output, exercising agreement between the header and library. +[tests/header_test.rs](tests/header_test.rs) checks that the cbindgen output +matches the checked-in header. The Rust ABI and header tests run on every CI +platform; they do not qualify a compiled C caller outside Linux. On Linux the +smoke test requires a C compiler and the static library; the header check always +requires `cbindgen`. Missing prerequisites fail rather than skip these checks. diff --git a/qodec/bindings/c/cbindgen.toml b/qodec/bindings/c/cbindgen.toml new file mode 100644 index 00000000..09dcc637 --- /dev/null +++ b/qodec/bindings/c/cbindgen.toml @@ -0,0 +1,59 @@ +# cbindgen configuration for the qodec C header. +# +# The header is checked in at include/qodec.h and a test asserts it stays in +# sync with the Rust source. Regenerate with ./regenerate.sh after changing any +# `extern "C"` signature or status constant. + +language = "C" +header = """/* + * qodec.h - C ABI for qodec. + * + * GENERATED by cbindgen from the qodec-c crate. DO NOT EDIT BY HAND. + * Regenerate with: bindings/c/regenerate.sh + * + * Source of truth: bindings/c/src/lib.rs. + * + * Load with qodec_load, read the returned Qodec and its fields, then release + * it with qodec_unload. Treat the root and all reachable storage as read-only. + * + * Conventions: + * - Ordinary collections are { count, items }; iterate 0 .. count. + * - Parity equations, string lists and selection patterns use flat storage + * with count + 1 offsets when nonempty. Do not read offsets when empty. + * - Strings are borrowed, NUL-terminated const char *. An absent optional + * string is NULL; an absent collection is empty. + * A string containing NUL fails C projection with QODEC_STATUS_ERROR. + * - QodecAction and QodecArgumentValue are tagged unions: switch on tag + * and read only the matching member. QodecReference is a flat struct. + * - Operands are blocks; parameters are declared classical inputs; arguments + * are values supplied to those parameters. QodecInstructionCall separates + * operands from arguments; both contain QodecArgument entries. + * - QodecParameter.kind describes the declared parameter type, while + * QodecArgumentValue.tag selects the supplied value's representation. + * - int32_t status codes; QODEC_STATUS_OK (0) is success and negative values + * are errors. After a failing status, qodec_last_error() returns a message + * for the calling thread, valid until its next recorded error. + * - A panic caught in qodec_load returns QODEC_STATUS_PANIC; a panic caught + * in qodec_find_gadget returns NULL. Both record an error message. + * - Every nested pointer borrows from the root and becomes invalid on unload. + */""" + +include_guard = "QODEC_H" +pragma_once = false +cpp_compat = true +documentation = true +documentation_style = "doxy" +style = "type" +usize_is_size_t = true + +[export] +include = [ + "QODEC_ABI_VERSION", + "QODEC_STATUS_OK", + "QODEC_STATUS_ERROR", + "QODEC_STATUS_INVALID_ARG", + "QODEC_STATUS_PANIC", +] + +[enum] +prefix_with_name = true diff --git a/qodec/bindings/c/examples/demo.c b/qodec/bindings/c/examples/demo.c new file mode 100644 index 00000000..9349417d --- /dev/null +++ b/qodec/bindings/c/examples/demo.c @@ -0,0 +1,234 @@ +/* + * Minimal C consumer of the qodec ABI: open a qodec, walk its lowering chain + * and one gadget's decoding surface, then release it. + * + * Note what is absent: no malloc, no free, no accessor calls. qodec_load + * hands back the root and it is all struct traversal from there. + * + * Built and run by tests/c_smoke_test.rs. + */ +#include + +#include "qodec.h" + +/* Every string list is CSR: a NUL-terminated blob plus offsets into it. */ +static void print_strings(const QodecStrings *list) { + for (size_t i = 0; i < list->count; ++i) { + printf("%s%s", i ? " " : "", list->bytes + list->offsets[i]); + } +} + +/* QodecArgument represents both block operands and parameter arguments. + * Its value.tag selects the supplied representation, not a declared parameter type. */ +static void print_argument(const QodecArgument *a) { + if (a->name) { + printf(" %s=", a->name); + } else { + printf(" "); + } + switch (a->value.tag) { + case QodecArgumentValue_Qubit: + printf("q%llu", (unsigned long long)a->value.qubit.index); + break; + case QodecArgumentValue_Readout: + printf("rec[%llu]", (unsigned long long)a->value.readout.index); + break; + case QodecArgumentValue_Integer: + printf("%lld", (long long)a->value.integer.value); + break; + case QodecArgumentValue_Number: + printf("%g", a->value.number.value); + break; + case QodecArgumentValue_Boolean: + printf("%s", a->value.boolean.value ? "true" : "false"); + break; + case QodecArgumentValue_Text: + printf("%s", a->value.text.value); + break; + case QodecArgumentValue_QubitList: + printf("["); + for (size_t i = 0; i < a->value.qubit_list.qubits.count; ++i) { + printf("%sq%llu", i ? " " : "", (unsigned long long)a->value.qubit_list.qubits.items[i]); + } + printf("]"); + break; + case QodecArgumentValue_StringList: + printf("["); + print_strings(&a->value.string_list.strings); + printf("]"); + break; + default: + printf("", (unsigned)a->value.tag); + break; + } +} + +/* The parsed circuit: a C consumer never writes a stim or YAML parser. */ +static void print_program(const QodecCircuit *circuit) { + if (circuit->error) { + printf(" circuit did not parse: %s\n", circuit->error); + return; + } + printf(" program: %zu calls\n", circuit->calls.count); + for (size_t i = 0; i < circuit->calls.count; ++i) { + const QodecInstructionCall *call = &circuit->calls.items[i]; + printf(" %s", call->mnemonic); + for (size_t s = 0; s < call->operands.count; ++s) { + print_argument(&call->operands.items[s]); + } + for (size_t o = 0; o < call->arguments.count; ++o) { + print_argument(&call->arguments.items[o]); + } + for (size_t p = 0; p < call->select.count; ++p) { + printf(" select{"); + for (size_t c = call->select.offsets[p]; c < call->select.offsets[p + 1]; ++c) { + printf("%s%s=%u", c == call->select.offsets[p] ? "" : " ", call->select.constraints[c].flag, + (unsigned)call->select.constraints[c].bit); + } + printf("}"); + } + printf("\n"); + } +} + +/* QodecAction and QodecArgumentValue are tagged unions: read the arm the tag names. */ +static void print_action(const QodecInstruction *instruction) { + for (size_t s = 0; s < instruction->action.count; ++s) { + const QodecAction *a = &instruction->action.items[s].action; + printf(" action[%zu]: ", s); + switch (a->tag) { + case QodecAction_Stabilize: + printf("stabilize "); + print_strings(&a->stabilize.paulis); + break; + case QodecAction_Observe: + printf("observe "); + print_strings(&a->observe.observables); + break; + case QodecAction_Pauli: + printf("pauli %s", a->pauli.pauli); + break; + case QodecAction_Clifford: + printf("clifford"); + for (size_t i = 0; i < a->clifford.from.count; ++i) { + printf(" %s->%s", a->clifford.from.bytes + a->clifford.from.offsets[i], + a->clifford.to.bytes + a->clifford.to.offsets[i]); + } + break; + case QodecAction_Rotate: + printf("rotate %s by ", a->rotate.axis); + if (a->rotate.angle_is_literal) { + printf("%g", a->rotate.angle_literal); + } else { + printf("<%s>", a->rotate.angle_operand); + } + break; + default: + printf("", (unsigned)a->tag); + break; + } + printf("\n"); + } +} + +static void print_reference(const QodecReference *r) { + switch (r->tag) { + case QODEC_REFERENCE_CONSTANT: + printf(" %llu", (unsigned long long)r->index); + break; + case QODEC_REFERENCE_CIRCUIT_READOUT: + printf(" circuit.readouts[%llu]", (unsigned long long)r->index); + break; + case QODEC_REFERENCE_READOUT: + printf(" readouts[%llu]", (unsigned long long)r->index); + break; + case QODEC_REFERENCE_ENCODING_PROPERTY: + printf(" %s[%llu].%s[%llu]", r->boundary == QODEC_BOUNDARY_IN ? "in" : "out", (unsigned long long)r->entry, + r->property == QODEC_PROPERTY_STABILIZER ? "stabilizers" + : r->property == QODEC_PROPERTY_LOGICAL_X ? "x" + : "z", + (unsigned long long)r->index); + break; + default: + printf(" ", (unsigned)r->tag); + break; + } +} + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + + if (qodec_abi_version() != QODEC_ABI_VERSION) { + fprintf(stderr, "ABI mismatch: header %d, library %u\n", QODEC_ABI_VERSION, qodec_abi_version()); + return 1; + } + + Qodec *qodec = NULL; + if (qodec_load(argv[1], &qodec) != QODEC_STATUS_OK) { + fprintf(stderr, "open failed: %s\n", qodec_last_error()); + return 1; + } + + printf("qodec: %s\n", qodec->name); + printf("layers: %zu\n", qodec->layers.count); + + for (size_t i = 0; i < qodec->layers.count; ++i) { + const QodecLayer *layer = &qodec->layers.items[i]; + printf("layer %zu: %s (%zu blocks, %zu instructions, %zu gadgets)\n", i, layer->instruction_set_name, layer->blocks.count, + layer->instructions.count, layer->gadgets.count); + } + + if (qodec->layers.count == 0 || qodec->layers.items[0].gadgets.count == 0) { + printf("no gadgets to inspect\nok\n"); + qodec_unload(qodec); + return 0; + } + + /* Inspect whichever gadget the top layer lists first. */ + const QodecGadget *gadget = &qodec->layers.items[0].gadgets.items[0]; + /* The formal semantics of every instruction the top ISA declares. */ + const QodecLayer *top = &qodec->layers.items[0]; + for (size_t i = 0; i < top->instructions.count; ++i) { + const QodecInstruction *instruction = &top->instructions.items[i]; + printf("%s(%zu in, %zu out, %zu params, %zu flags)\n", instruction->mnemonic, instruction->inputs.count, + instruction->outputs.count, instruction->parameters.count, instruction->flags.count); + print_action(instruction); + } + + printf("inspecting gadget: %s\n", gadget->implements.mnemonic); + printf(" circuit targets %s (%s)\n", gadget->circuit.instruction_set_name, gadget->circuit.effective_format); + print_program(&gadget->circuit); + + printf("checks: %zu (%zu references)\n", gadget->checks.count, gadget->checks.total); + for (size_t i = 0; i < gadget->checks.count; ++i) { + printf(" check %zu:", i); + for (size_t j = gadget->checks.offsets[i]; j < gadget->checks.offsets[i + 1]; ++j) { + print_reference(&gadget->checks.references[j]); + } + printf("\n"); + } + + for (size_t e = 0; e < gadget->inputs.count; ++e) { + const QodecEncoding *encoding = &gadget->inputs.items[e]; + const QodecCode *code = &encoding->code; + printf("in[%zu]: code %s, %zu stabilizers\n", e, code->name, code->stabilizers.count); + for (size_t s = 0; s < code->stabilizers.count; ++s) { + printf(" %s\n", code->stabilizers.bytes + code->stabilizers.offsets[s]); + } + } + + /* A deliberate miss, to show the null/last_error pairing works. */ + if (qodec_find_gadget(&qodec->layers.items[0], "no_such_instruction") != NULL) { + fprintf(stderr, "expected NULL for an unknown mnemonic\n"); + qodec_unload(qodec); + return 1; + } + printf("unknown mnemonic rejected: %s\n", qodec_last_error()); + + qodec_unload(qodec); + printf("ok\n"); + return 0; +} diff --git a/qodec/bindings/c/include/qodec.h b/qodec/bindings/c/include/qodec.h new file mode 100644 index 00000000..447d8443 --- /dev/null +++ b/qodec/bindings/c/include/qodec.h @@ -0,0 +1,1090 @@ +/* + * qodec.h - C ABI for qodec. + * + * GENERATED by cbindgen from the qodec-c crate. DO NOT EDIT BY HAND. + * Regenerate with: bindings/c/regenerate.sh + * + * Source of truth: bindings/c/src/lib.rs. + * + * Load with qodec_load, read the returned Qodec and its fields, then release + * it with qodec_unload. Treat the root and all reachable storage as read-only. + * + * Conventions: + * - Ordinary collections are { count, items }; iterate 0 .. count. + * - Parity equations, string lists and selection patterns use flat storage + * with count + 1 offsets when nonempty. Do not read offsets when empty. + * - Strings are borrowed, NUL-terminated const char *. An absent optional + * string is NULL; an absent collection is empty. + * A string containing NUL fails C projection with QODEC_STATUS_ERROR. + * - QodecAction and QodecArgumentValue are tagged unions: switch on tag + * and read only the matching member. QodecReference is a flat struct. + * - Operands are blocks; parameters are declared classical inputs; arguments + * are values supplied to those parameters. QodecInstructionCall separates + * operands from arguments; both contain QodecArgument entries. + * - QodecParameter.kind describes the declared parameter type, while + * QodecArgumentValue.tag selects the supplied value's representation. + * - int32_t status codes; QODEC_STATUS_OK (0) is success and negative values + * are errors. After a failing status, qodec_last_error() returns a message + * for the calling thread, valid until its next recorded error. + * - A panic caught in qodec_load returns QODEC_STATUS_PANIC; a panic caught + * in qodec_find_gadget returns NULL. Both record an error message. + * - Every nested pointer borrows from the root and becomes invalid on unload. + */ + +#ifndef QODEC_H +#define QODEC_H + +#include +#include +#include +#include +#include + +/** + * ABI revision. Bump for any breaking change to a signature, symbol, struct + * layout or calling convention below. + */ +#define QODEC_ABI_VERSION 1 + +/** + * The call succeeded. + */ +#define QODEC_STATUS_OK 0 + +/** + * The qodec could not be loaded; see `qodec_last_error()`. + */ +#define QODEC_STATUS_ERROR -1 + +/** + * `qodec_load` received a null argument or a non-UTF-8 path. + */ +#define QODEC_STATUS_INVALID_ARG -2 + +/** + * A Rust panic was caught during `qodec_load`; see `qodec_last_error()`. + */ +#define QODEC_STATUS_PANIC -3 + +/** + * `QodecReference.tag`: `circuit.readouts[index]`, a measurement-record bit + * at a zero-based position in measurement order. + */ +#define QODEC_REFERENCE_CIRCUIT_READOUT 0 + +/** + * `QodecReference.tag`: `readouts[index]`, a zero-based gadget readout position. + */ +#define QODEC_REFERENCE_READOUT 1 + +/** + * `QodecReference.tag`: `{in,out}[entry].{stabilizers,x,z}[index]`, an + * encoding sign. Only this tag uses `boundary`, `property` and `entry`. + */ +#define QODEC_REFERENCE_ENCODING_PROPERTY 2 + +/** + * A literal parity bit. `index` is 0 or 1; all other fields are zero. + */ +#define QODEC_REFERENCE_CONSTANT 3 + +/** + * `QodecReference.boundary`: the gadget's `inputs` (`in:` on disk). + */ +#define QODEC_BOUNDARY_IN 0 + +/** + * `QodecReference.boundary`: the gadget's `outputs` (`out:` on disk). + */ +#define QODEC_BOUNDARY_OUT 1 + +/** + * `QodecReference.property` — a stabilizer-generator sign. + */ +#define QODEC_PROPERTY_STABILIZER 0 + +/** + * `QodecReference.property` — a logical-X operator sign. + */ +#define QODEC_PROPERTY_LOGICAL_X 1 + +/** + * `QodecReference.property` — a logical-Z operator sign. + */ +#define QODEC_PROPERTY_LOGICAL_Z 2 + +/** + * `QodecParameter.kind`: a runtime classical bit parameter, eligible in conditions. + */ +#define QODEC_PARAMETER_BIT 0 + +/** + * `QodecParameter.kind`: a parameter accepting a compile-time real literal. + */ +#define QODEC_PARAMETER_NUMBER 1 + +/** + * `QodecParameter.kind`: a parameter accepting a compile-time integer literal. + */ +#define QODEC_PARAMETER_INTEGER 2 + +/** + * `QodecParameter.kind`: a parameter accepting a compile-time boolean literal. + */ +#define QODEC_PARAMETER_BOOLEAN 3 + +/** + * `QodecParameter.kind`: a parameter accepting a compile-time string literal. + */ +#define QODEC_PARAMETER_STRING 4 + +/** + * `QodecParameter.kind`: a parameter accepting a compile-time Pauli literal. + */ +#define QODEC_PARAMETER_PAULI 5 + +/** + * A block type declared by an instruction set. + */ +typedef struct { + /** + * The block type's name, as instruction operands reference it. + */ + const char *name; + /** + * How many logical qubits a block of this type encodes. + */ + size_t encodes; +} QodecBlock; + +/** + * A borrowed run of block declarations. + */ +typedef struct { + /** + * How many block declarations there are. + */ + size_t count; + /** + * The declarations; null when `count` is zero. + */ + const QodecBlock *items; +} QodecBlocks; + +/** + * One positional block operand in an instruction's `in:` / `out:` list. + * + * Operands are nameless: position fixes the contiguous range the entry + * occupies in the flat index space the action addresses. + */ +typedef struct { + /** + * The block type this entry's qubits are encoded in. + */ + const char *block; + /** + * Whether the entry is variadic (`[block]` on disk). + */ + bool is_variadic; +} QodecBlockOperand; + +/** + * A borrowed run of block operands. + */ +typedef struct { + /** + * How many operands there are. + */ + size_t count; + /** + * The operands; null when `count` is zero. + */ + const QodecBlockOperand *items; +} QodecBlockOperands; + +/** + * A list of strings, in compressed-sparse-row form. + * + * For `string_index < count`, `bytes + offsets[string_index]` is a borrowed, + * NUL-terminated UTF-8 string. Offsets include the terminators. A string + * containing NUL fails C projection. When `count == 0`, do not read either buffer. + * + * ```c + * const QodecStrings *stabilizers = &encoding->code.stabilizers; + * for (size_t string_index = 0; string_index < stabilizers->count; ++string_index) { + * puts(stabilizers->bytes + stabilizers->offsets[string_index]); + * } + * ``` + */ +typedef struct { + /** + * How many strings there are. + */ + size_t count; + /** + * `count + 1` offsets into `bytes`; null when `count` is zero. + */ + const size_t *offsets; + /** + * The strings, concatenated, each NUL-terminated; null when `count` is zero. + */ + const char *bytes; + /** + * Total length of `bytes` including every terminator. + */ + size_t total; +} QodecStrings; + +/** + * A declared classical input to an instruction. + */ +typedef struct { + /** + * The parameter's name, as call sites and conditions reference it. + */ + const char *name; + /** + * Its declared parameter type; one of the `QODEC_PARAMETER_*` values. + * `BIT` is the runtime, condition-eligible type; the rest accept compile-time + * literals. This is not a `QodecArgumentValue` tag. + */ + uint8_t kind; +} QodecParameter; + +/** + * A borrowed run of parameter declarations. + */ +typedef struct { + /** + * How many parameters there are. + */ + size_t count; + /** + * The parameters; null when `count` is zero. + */ + const QodecParameter *items; +} QodecParameters; + +/** + * One operation in an instruction's formal semantics. + * + * In C, switch on `tag` and read only its matching union member. For example, + * `QodecAction_Observe` selects `observe.observables`. + */ +enum QodecAction_Tag +#if defined(__cplusplus) || __STDC_VERSION__ >= 202311L + : uint8_t +#endif // defined(__cplusplus) || __STDC_VERSION__ >= 202311L + { + /** + * Force the state into the +1 eigenspace of every operator listed. + */ + QodecAction_Stabilize, + /** + * A Clifford unitary, as the tableau mapping `from[i]` to `to[i]`. + */ + QodecAction_Clifford, + /** + * Apply a Pauli unitary. + */ + QodecAction_Pauli, + /** + * Measure each observable, one classical bit per entry, in order. + */ + QodecAction_Observe, + /** + * Rotate about `axis` by a literal angle or a referenced parameter's value. + */ + QodecAction_Rotate, +}; +#ifndef __cplusplus +#if __STDC_VERSION__ >= 202311L +typedef enum QodecAction_Tag QodecAction_Tag; +#else +typedef uint8_t QodecAction_Tag; +#endif // __STDC_VERSION__ >= 202311L +#endif // __cplusplus + +typedef struct { + QodecStrings paulis; +} QodecAction_Stabilize_Body; + +typedef struct { + QodecStrings from; + QodecStrings to; +} QodecAction_Clifford_Body; + +typedef struct { + const char *pauli; +} QodecAction_Pauli_Body; + +typedef struct { + QodecStrings observables; +} QodecAction_Observe_Body; + +typedef struct { + const char *axis; + /** + * Whether the angle is a literal rather than a parameter reference. + */ + bool angle_is_literal; + /** + * The angle, when `angle_is_literal`. + */ + double angle_literal; + /** + * Parameter name when `angle_is_literal` is false; null for a literal. + * `angle_operand` is the ABI field name for this parameter reference. + */ + const char *angle_operand; +} QodecAction_Rotate_Body; + +typedef struct { + QodecAction_Tag tag; + union { + QodecAction_Stabilize_Body stabilize; + QodecAction_Clifford_Body clifford; + QodecAction_Pauli_Body pauli; + QodecAction_Observe_Body observe; + QodecAction_Rotate_Body rotate; + }; +} QodecAction; + +/** + * One step of an instruction's action, with its optional guard. + */ +typedef struct { + /** + * The operation this step performs. + */ + QodecAction action; + /** + * Whether the step is guarded. + */ + bool has_condition; + /** + * The bits XOR-ed to form the guard, when `has_condition`. + */ + QodecStrings condition_predicates; + /** + * Whether the guard is inverted (`unless` rather than `if`), when `has_condition`. + */ + bool condition_invert; +} QodecActionStep; + +/** + * A borrowed run of action steps. + */ +typedef struct { + /** + * How many steps there are. + */ + size_t count; + /** + * The steps; null when `count` is zero. + */ + const QodecActionStep *items; +} QodecActionSteps; + +/** + * One instruction declared by an instruction set. + * + * `QodecAction_Observe` steps declare measurement outcomes in action order. + * `flags` declares additional named output bits. + */ +typedef struct { + /** + * The instruction's mnemonic. + */ + const char *mnemonic; + /** + * Free-text description, or the empty string. + */ + const char *description; + /** + * Input block operands (logical qubits consumed or passed through). + */ + QodecBlockOperands inputs; + /** + * Output block operands (logical qubits produced or passed through). + */ + QodecBlockOperands outputs; + /** + * Named classical outputs following the action's measurement outcomes. + */ + QodecStrings flags; + /** + * Declared classical inputs; calls supply arguments for these parameters. + */ + QodecParameters parameters; + /** + * The formal semantics: an ordered list of guarded operations. + */ + QodecActionSteps action; + /** + * Free-form annotations as JSON object text, including `{}` when empty. + */ + const char *metadata_json; +} QodecInstruction; + +/** + * A borrowed run of instructions, in declaration order. + */ +typedef struct { + /** + * How many instructions there are. + */ + size_t count; + /** + * The instructions; null when `count` is zero. + */ + const QodecInstruction *items; +} QodecInstructions; + +/** + * A borrowed run of numeric circuit-qubit identifiers, not array positions. + * + * Identifiers are `uint64_t` wherever they appear, including + * `QodecArgumentValue.qubit.index`; counts are `size_t`. + */ +typedef struct { + /** + * How many indices there are. + */ + size_t count; + /** + * The indices; null when `count` is zero. + */ + const uint64_t *items; +} QodecIndices; + +/** + * A block label or an argument value supplied at a call site. + * + * Used by both `QodecInstructionCall.operands` and + * `QodecInstructionCall.arguments`. The tag describes the supplied + * representation, not the declared `QodecParameter.kind`. + * + * In C, switch on `tag` and read only its matching union member. For example, + * `QodecArgumentValue_Qubit` selects `qubit.index`, not `integer.value`. + */ +enum QodecArgumentValue_Tag +#if defined(__cplusplus) || __STDC_VERSION__ >= 202311L + : uint8_t +#endif // defined(__cplusplus) || __STDC_VERSION__ >= 202311L + { + /** + * A numeric block label or qubit identifier, not a position in a projected array. + */ + QodecArgumentValue_Qubit, + /** + * A list of numeric circuit-qubit identifiers. + */ + QodecArgumentValue_QubitList, + /** + * An integer literal. + */ + QodecArgumentValue_Integer, + /** + * A real literal. + */ + QodecArgumentValue_Number, + /** + * A block label carried as text, or a string-valued argument. + */ + QodecArgumentValue_Text, + /** + * A list of string literals. + */ + QodecArgumentValue_StringList, + /** + * A prior measurement-record bit at an absolute, zero-based position in + * measurement order across preceding calls. + */ + QodecArgumentValue_Readout, + /** + * A Boolean literal, distinct from integer literals 0 and 1. + */ + QodecArgumentValue_Boolean, +}; +#ifndef __cplusplus +#if __STDC_VERSION__ >= 202311L +typedef enum QodecArgumentValue_Tag QodecArgumentValue_Tag; +#else +typedef uint8_t QodecArgumentValue_Tag; +#endif // __STDC_VERSION__ >= 202311L +#endif // __cplusplus + +typedef struct { + uint64_t index; +} QodecArgumentValue_Qubit_Body; + +typedef struct { + QodecIndices qubits; +} QodecArgumentValue_QubitList_Body; + +typedef struct { + int64_t value; +} QodecArgumentValue_Integer_Body; + +typedef struct { + double value; +} QodecArgumentValue_Number_Body; + +typedef struct { + const char *value; +} QodecArgumentValue_Text_Body; + +typedef struct { + QodecStrings strings; +} QodecArgumentValue_StringList_Body; + +typedef struct { + uint64_t index; +} QodecArgumentValue_Readout_Body; + +typedef struct { + bool value; +} QodecArgumentValue_Boolean_Body; + +typedef struct { + QodecArgumentValue_Tag tag; + union { + QodecArgumentValue_Qubit_Body qubit; + QodecArgumentValue_QubitList_Body qubit_list; + QodecArgumentValue_Integer_Body integer; + QodecArgumentValue_Number_Body number; + QodecArgumentValue_Text_Body text; + QodecArgumentValue_StringList_Body string_list; + QodecArgumentValue_Readout_Body readout; + QodecArgumentValue_Boolean_Body boolean; + }; +} QodecArgumentValue; + +/** + * One block operand or parameter argument at a call site. + */ +typedef struct { + /** + * The parameter name for a named argument; null for a positional block operand. + */ + const char *name; + /** + * The block label or supplied argument value. + */ + QodecArgumentValue value; +} QodecArgument; + +/** + * A borrowed run of block operands or parameter arguments. + */ +typedef struct { + /** + * How many entries there are. + */ + size_t count; + /** + * The entries; null when `count` is zero. + */ + const QodecArgument *items; +} QodecArguments; + +/** + * One constraint in a `select` pattern: a flag bit and its expected value. + */ +typedef struct { + /** + * The call's flag, named or addressed by its zero-based `flags[]` position. + */ + const char *flag; + /** + * The value it is expected to take, `0` or `1`. + */ + uint8_t bit; +} QodecSelectConstraint; + +/** + * Selection patterns over a call's own flags, in compressed-sparse-row form. + * + * Pattern `pattern_index` uses the borrowed range + * `constraints[offsets[pattern_index] .. offsets[pattern_index + 1]]`. + * A pattern matches when every constraint matches; selection matches when any + * pattern matches. `count == 0` imposes no constraint. + * Do not read offsets when `count == 0` or constraints when `total == 0`. + */ +typedef struct { + /** + * How many patterns there are. + */ + size_t count; + /** + * `count + 1` offsets delimiting `constraints`; null when `count` is zero. + */ + const size_t *offsets; + /** + * The patterns' constraints, concatenated. + */ + const QodecSelectConstraint *constraints; + /** + * Total number of constraints. Zero when `count == 0`; otherwise `offsets[count]`. + */ + size_t total; +} QodecSelect; + +/** + * One invocation of a `QodecInstruction` in a circuit. + */ +typedef struct { + /** + * The mnemonic invoked, naming an instruction in the circuit's instruction set. + */ + const char *mnemonic; + /** + * Blocks in the instruction's declared operand order. Each `name` is null; + * its value is a numeric label (`Qubit`) or a named label (`Text`). + */ + QodecArguments operands; + /** + * Arguments supplied to the instruction's declared parameters, each carrying + * the parameter name in `name`. + */ + QodecArguments arguments; + /** + * Selection patterns over this call's flags; empty when no selection is specified. + */ + QodecSelect select; +} QodecInstructionCall; + +/** + * A borrowed run of instruction calls, in program order. + */ +typedef struct { + /** + * How many calls there are. + */ + size_t count; + /** + * The calls; null when `count` is zero. + */ + const QodecInstructionCall *items; +} QodecInstructionCalls; + +/** + * A gadget's circuit: the program that runs, and the instruction set it calls into. + */ +typedef struct { + /** + * The name of the target instruction set the source calls into. + */ + const char *instruction_set_name; + /** + * Inlined program text, preserved verbatim. NUL bytes fail C projection. + */ + const char *source; + /** + * The resolved source-format tag, or null when inferred from the text. + */ + const char *format; + /** + * The format selected for parsing: `format` when non-null, otherwise + * inferred from the text. This does not guarantee a parser is available. + */ + const char *effective_format; + /** + * Calls parsed and checked against the target instruction set during `qodec_load`. + * Empty on a parse error, an undeclared instruction, or a program with no + * calls. Check `error` to distinguish failure from an empty program. + */ + QodecInstructionCalls calls; + /** + * Parse or instruction-check error, or null on success. This error leaves + * `calls` empty without failing `qodec_load` and is not recorded in + * `qodec_last_error`. + */ + const char *error; +} QodecCircuit; + +/** + * One term of a parity equation, in fully expanded form. + * + * `tag` selects the reference kind. `boundary`, `property` and `entry` are + * meaningful only for `QODEC_REFERENCE_ENCODING_PROPERTY` and are zero + * otherwise. Every index is relative to the containing gadget. + * + * Selectors are already expanded: an authored `circuit.readouts[0,2,5]` is + * three references. + */ +typedef struct { + /** + * Which reference shape this is; one of the `QODEC_REFERENCE_*` values. + */ + uint8_t tag; + /** + * For an encoding property, one of the `QODEC_BOUNDARY_*` values. + */ + uint8_t boundary; + /** + * For an encoding property, one of the `QODEC_PROPERTY_*` values. + */ + uint8_t property; + /** + * For an encoding property, the zero-based encoding position in the + * gadget's `inputs` or `outputs`, selected by `boundary`. + */ + uint64_t entry; + /** + * Zero-based circuit or gadget readout position; for an encoding property, + * the operator position in the encoding code's `stabilizers`, `x` or `z`. + * For `QODEC_REFERENCE_CONSTANT`, the literal bit 0 or 1. + */ + uint64_t index; +} QodecReference; + +/** + * A list of parity equations, in compressed-sparse-row form. + * + * Equation `equation_index` uses the borrowed range + * `references[offsets[equation_index] .. offsets[equation_index + 1]]`. + * This storage is shared by checks, readouts, and frames; only checks assert zero parity. + * Do not read offsets when `count == 0` or references when `total == 0`. + */ +typedef struct { + /** + * How many equations there are. + */ + size_t count; + /** + * `count + 1` offsets delimiting `references`; null when `count` is zero. + */ + const size_t *offsets; + /** + * The equations' references, concatenated. + */ + const QodecReference *references; + /** + * Total number of references. Zero when `count == 0`; otherwise `offsets[count]`. + */ + size_t total; +} QodecParity; + +/** + * A quantum error-correcting code. + */ +typedef struct { + /** + * The code's name. + */ + const char *name; + /** + * Free-text description, or the empty string. + */ + const char *description; + /** + * Stabilizer generators, as Pauli strings. + */ + QodecStrings stabilizers; + /** + * Logical X operators, one per logical qubit. + */ + QodecStrings x; + /** + * Logical Z operators, one per logical qubit, aligned with `x`. + */ + QodecStrings z; + /** + * Number of logical qubits, equal to `x.count`. A valid code declares as + * many logical Z operators, but a draft need not: bound `z` by `z.count`. + */ + size_t logical_count; + /** + * One more than the highest qubit index across stabilizers and logical + * operators, or zero when none is used. + */ + uint64_t physical_qubit_count; + /** + * Free-form annotations as JSON object text, including `{}` when empty. + */ + const char *metadata_json; +} QodecCode; + +/** + * One boundary encoding: the code a gadget operand is encoded in, and where + * that code's blocks land in the circuit. + */ +typedef struct { + /** + * The code this encoding uses. + */ + QodecCode code; + /** + * Circuit-operand labels in code-block order, such as `"0"` or `"ancilla"`. + * These are labels, not positions in a projected array. + */ + QodecStrings support; + /** + * Block-type names parallel to `support`, or an empty list if unavailable. + */ + QodecStrings block_types; +} QodecEncoding; + +/** + * A borrowed run of encodings. + */ +typedef struct { + /** + * How many encodings there are. + */ + size_t count; + /** + * The encodings; null when `count` is zero. + */ + const QodecEncoding *items; +} QodecEncodings; + +/** + * One gadget: the per-instruction rule lowering it to the layer below. + */ +typedef struct { + /** + * The instruction this gadget declares it implements. + */ + QodecInstruction implements; + /** + * The circuit supplied for this implementation. + */ + QodecCircuit circuit; + /** + * Declared zero-parity checks. Loading does not verify them against the circuit. + */ + QodecParity checks; + /** + * Readout equations in output order: the instruction's `observe` outcomes + * first, then its flags. These are not all zero-parity checks. + */ + QodecParity readouts; + /** + * One name per readout, parallel to `readouts`; an empty string for an + * anonymous readout. Names are labels, not reference indices. + */ + QodecStrings readout_names; + /** + * Number of `observe` outcomes declared by the implemented instruction. + * For `readout_index < readouts.count`, `readout_index >= observe_count` + * identifies a flag. An incomplete draft may declare fewer readouts than + * outcomes, so this can exceed `readouts.count`. + */ + size_t observe_count; + /** + * Instruction parameter names forwarded into the circuit source, parallel + * to `parameter_targets`. + */ + QodecStrings parameter_names; + /** + * Circuit-source parameter names, parallel to `parameter_names`, without + * the on-disk `circuit.source.` prefix. + */ + QodecStrings parameter_targets; + /** + * Input boundary encodings. An encoding-property reference selects an + * entry here when its `boundary` is `QODEC_BOUNDARY_IN`. + */ + QodecEncodings inputs; + /** + * Output boundary encodings, indexed likewise for `QODEC_BOUNDARY_OUT`. + */ + QodecEncodings outputs; + /** + * Free-form annotations as JSON object text, including `{}` when empty. + */ + const char *metadata_json; + /** + * Output logical-sign reference paths in sorted order, parallel to `frames`. + */ + QodecStrings frame_targets; + /** + * Additional output-sign corrections as XOR equations. Empty entries apply + * no correction. These are definitions, not zero-parity checks. + */ + QodecParity frames; +} QodecGadget; + +/** + * A borrowed run of gadgets, in sorted mnemonic order. + * Ordering uses UTF-8 bytes. + */ +typedef struct { + /** + * How many gadgets there are. + */ + size_t count; + /** + * The gadgets; null when `count` is zero. + */ + const QodecGadget *items; +} QodecGadgets; + +/** + * One layer of the lowering chain: an instruction set plus the gadgets lowering it. + */ +typedef struct { + /** + * The name of this layer's instruction set. + */ + const char *instruction_set_name; + /** + * The instruction set's description, or the empty string. + */ + const char *instruction_set_description; + /** + * The block types the instruction set declares. + */ + QodecBlocks blocks; + /** + * The instruction set's instructions, in declaration order. + */ + QodecInstructions instructions; + /** + * The gadgets lowering this layer to the next. Empty on the bottom layer, + * which is the target of the layer above and lowers no further. + */ + QodecGadgets gadgets; + /** + * The instruction set's free-form annotations as JSON object text, including `{}` when empty. + */ + const char *instruction_set_metadata_json; +} QodecLayer; + +/** + * A borrowed run of layers, ordered logical to physical. + */ +typedef struct { + /** + * How many layers there are. + */ + size_t count; + /** + * The layers; null when `count` is zero. + */ + const QodecLayer *items; +} QodecLayers; + +/** + * The root of a loaded qodec. + * + * Produced by `qodec_load()` and released by `qodec_unload()`. Treat this struct + * and all reachable storage as read-only. All nested pointers borrow from + * this root and become invalid when it is unloaded. + * + * Pass the original root pointer to `qodec_unload` exactly once, not a copy of + * the struct. Never free the root or its nested storage yourself. + */ +typedef struct { + /** + * The qodec's name, or the empty string when the manifest has none. + */ + const char *name; + /** + * The manifest description, or the empty string. + */ + const char *description; + /** + * Whether the manifest declared an on-disk schema version. + */ + bool has_schema_version; + /** + * The declared on-disk schema version; meaningful only when `has_schema_version`. + */ + uint32_t schema_version; + /** + * The lowering chain, ordered logical to physical. + */ + QodecLayers layers; + /** + * The manifest's free-form annotations as JSON object text, including `{}` when empty. + */ + const char *metadata_json; +} Qodec; + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * The ABI revision this library implements. + * + * Compare it with `QODEC_ABI_VERSION` from the header before reading projected + * structs. Do not use the projection if the revisions differ. + */ +uint32_t qodec_abi_version(void); + +/** + * The calling thread's most recent error, or null if there is none. + * + * The returned NUL-terminated string is read-only and borrows thread-local + * storage. It remains valid until another error is recorded on this thread + * or the thread exits. Copy it to retain the message; do not free it. + * + * Successful calls do not clear an older error. Check the load status or + * lookup result before consulting this message. + * NUL bytes in diagnostic text are escaped as `\0`. + */ +const char *qodec_last_error(void); + +/** + * Load a qodec from a manifest file or single-file bundle at `path`. + * The path must name a file, not a directory, and must be UTF-8. + * + * Returns `QODEC_STATUS_OK` and writes the root to `*out_qodec` on success. + * The root and all reachable storage are read-only; release the root with + * `qodec_unload()`. + * + * On failure, leaves `*out_qodec` unchanged and records `qodec_last_error()`. + * Returns `QODEC_STATUS_INVALID_ARG` for a null argument or non-UTF-8 path, + * `QODEC_STATUS_ERROR` for a loader error (including a directory path) or a + * string containing NUL in the C projection, or + * `QODEC_STATUS_PANIC` for a caught Rust panic. Initialize the output pointer + * to null before calling. + * + * Caller requirements: + * + * `path` must point to a valid, readable NUL-terminated C string. `out_qodec` + * must be non-null, aligned and writable for one root pointer. Both must + * remain valid for the duration of the call. + */ +int32_t qodec_load(const char *path, Qodec **out_qodec); + +/** + * Release a qodec from `qodec_load()`, invalidating everything reachable from + * it. Null is a no-op. + * + * Caller requirements: + * + * A non-null `qodec` must be exactly the pointer `qodec_load()` produced, not + * previously unloaded and not a copy of the struct. The root and its reachable + * storage must not have been modified or freed. All readers must have finished; + * unloading must not race with any read. Unloading twice is undefined behavior. + */ +void qodec_unload(Qodec *qodec); + +/** + * Find the first gadget in `layer` whose `implements.mnemonic` matches + * `mnemonic`. The comparison is exact and case-sensitive. + * + * Returns a borrowed pointer, valid until the owning qodec is unloaded. Do not + * modify or free it. Returns null and records `qodec_last_error()` for no match, + * a null argument, or a caught Rust panic. + * + * Caller requirements: + * + * `layer` must be null or point to a valid, unmodified layer in a live qodec. + * `mnemonic` must point to a valid, readable NUL-terminated C string for the + * duration of the call. Keep the owning qodec alive during the call and while + * using the returned pointer; unloading must not race with either use. + */ +const QodecGadget *qodec_find_gadget(const QodecLayer *layer, const char *mnemonic); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* QODEC_H */ diff --git a/qodec/bindings/c/regenerate.sh b/qodec/bindings/c/regenerate.sh new file mode 100755 index 00000000..fd06aabe --- /dev/null +++ b/qodec/bindings/c/regenerate.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Regenerate the checked-in C header from the Rust source. +# +# Run after changing any `extern "C"` signature or status constant in +# src/lib.rs. `header_is_in_sync` in tests/header_test.rs fails if you forget. +set -euo pipefail + +cd "$(dirname "$0")" + +if ! command -v cbindgen >/dev/null 2>&1; then + echo "cbindgen not found; install it with: cargo install cbindgen --locked" >&2 + exit 1 +fi + +mkdir -p include +cbindgen --config cbindgen.toml --crate qodec-c --output include/qodec.h +echo "regenerated include/qodec.h" diff --git a/qodec/bindings/c/src/lib.rs b/qodec/bindings/c/src/lib.rs new file mode 100644 index 00000000..2ed1f87a --- /dev/null +++ b/qodec/bindings/c/src/lib.rs @@ -0,0 +1,1544 @@ +//! C bindings for qodec: load a qodec and read its lowering chain. +//! +//! [`qodec_load`] builds a read-only projection and returns its [`Qodec`] root. +//! Fields expose the layers, instruction sets, actions, gadgets, parsed circuit +//! calls, boundary encodings and codes. Metadata is JSON object text in +//! `metadata_json` fields (`instruction_set_metadata_json` on layers), including `{}` when empty. +//! Circuit qubit and measurement-record lists are not projected. +//! +//! There are five C functions: [`qodec_load`], [`qodec_unload`], +//! [`qodec_find_gadget`], [`qodec_abi_version`] and [`qodec_last_error`]. +//! Authoring and mutation use the Rust or Python API. +//! +//! # Ownership +//! +//! [`qodec_load`] produces a [`Qodec`] and [`qodec_unload`] releases it. +//! Treat the root and all reachable storage as read-only. Nested pointers, +//! strings and collections borrow from the root and become invalid on unload. +//! Pass the original root pointer to `qodec_unload` exactly once, not a copy of +//! the struct. Never free the root or its nested storage yourself. +//! Several threads may read it concurrently, but all readers must finish before +//! it is unloaded. Unloading must not race with a read. +//! +//! # Conventions +//! +//! - Ordinary collections are `{ count, items }`; iterate `0 .. count`. +//! Empty collections have null storage pointers; do not dereference them. +//! - Parity equations, string lists and selection patterns use flat storage +//! with `count + 1` offsets when `count > 0`. Do not read offsets when empty. +//! - Strings are borrowed, NUL-terminated UTF-8. A string containing NUL fails +//! C projection with `QODEC_STATUS_ERROR`. An absent optional string is null. +//! - [`QodecAction`] and [`QodecArgumentValue`] are C tagged unions: switch on +//! `tag` and read only the matching member. +//! - Operands are blocks; parameters are declared classical inputs; arguments +//! are values supplied to those parameters. [`QodecInstructionCall`] separates +//! `operands` from `arguments`; both contain [`QodecArgument`] entries. +//! - [`QodecParameter::kind`] describes the declared parameter type, while +//! [`QodecArgumentValue`]'s tag selects the supplied value's representation. +//! - [`QodecReference`] is a flat struct. Its `tag` selects the reference kind; +//! only encoding-property references use `boundary`, `property` and `entry`. +//! +//! # Stability +//! +//! Struct layouts are part of the ABI. Adding, removing or reordering fields +//! can break compatibility. Compare [`qodec_abi_version`] with the header's +//! `QODEC_ABI_VERSION` before reading structs; do not proceed on a mismatch. +//! The ABI revision is independent of the package and on-disk schema versions. +//! +//! # Errors +//! +//! Only [`qodec_load`] returns a status: `QODEC_STATUS_OK` on success or a +//! negative status on failure. [`qodec_find_gadget`] returns null on failure. +//! Both record errors through [`qodec_last_error`], including caught Rust +//! panics. Successful calls do not clear an older error. + +use std::cell::RefCell; +use std::ffi::{CStr, CString, NulError, c_char}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::Path; + +use qodec::{Action, ActionStep, BlockOperand, Instruction, Parameter, ParameterKind, Scalar}; +use qodec::{Argument, InstructionCall, Operand, SelectPattern}; +use qodec::{Circuit, Encoding, Gadget}; +use qodec::{EncodingPropertyKind, GadgetBoundary, ParityTerm, ReferenceTarget}; + +/// ABI revision. Bump for any breaking change to a signature, symbol, struct +/// layout or calling convention below. +pub const QODEC_ABI_VERSION: u32 = 1; + +/// The call succeeded. +pub const QODEC_STATUS_OK: i32 = 0; +/// The qodec could not be loaded; see `qodec_last_error()`. +pub const QODEC_STATUS_ERROR: i32 = -1; +/// `qodec_load` received a null argument or a non-UTF-8 path. +pub const QODEC_STATUS_INVALID_ARG: i32 = -2; +/// A Rust panic was caught during `qodec_load`; see `qodec_last_error()`. +pub const QODEC_STATUS_PANIC: i32 = -3; + +/// `QodecReference.tag`: `circuit.readouts[index]`, a measurement-record bit +/// at a zero-based position in measurement order. +pub const QODEC_REFERENCE_CIRCUIT_READOUT: u8 = 0; +/// `QodecReference.tag`: `readouts[index]`, a zero-based gadget readout position. +pub const QODEC_REFERENCE_READOUT: u8 = 1; +/// `QodecReference.tag`: `{in,out}[entry].{stabilizers,x,z}[index]`, an +/// encoding sign. Only this tag uses `boundary`, `property` and `entry`. +pub const QODEC_REFERENCE_ENCODING_PROPERTY: u8 = 2; +/// A literal parity bit. `index` is 0 or 1; all other fields are zero. +pub const QODEC_REFERENCE_CONSTANT: u8 = 3; + +/// `QodecReference.boundary`: the gadget's `inputs` (`in:` on disk). +pub const QODEC_BOUNDARY_IN: u8 = 0; +/// `QodecReference.boundary`: the gadget's `outputs` (`out:` on disk). +pub const QODEC_BOUNDARY_OUT: u8 = 1; + +/// `QodecReference.property` — a stabilizer-generator sign. +pub const QODEC_PROPERTY_STABILIZER: u8 = 0; +/// `QodecReference.property` — a logical-X operator sign. +pub const QODEC_PROPERTY_LOGICAL_X: u8 = 1; +/// `QodecReference.property` — a logical-Z operator sign. +pub const QODEC_PROPERTY_LOGICAL_Z: u8 = 2; + +/// `QodecParameter.kind`: a runtime classical bit parameter, eligible in conditions. +pub const QODEC_PARAMETER_BIT: u8 = 0; +/// `QodecParameter.kind`: a parameter accepting a compile-time real literal. +pub const QODEC_PARAMETER_NUMBER: u8 = 1; +/// `QodecParameter.kind`: a parameter accepting a compile-time integer literal. +pub const QODEC_PARAMETER_INTEGER: u8 = 2; +/// `QodecParameter.kind`: a parameter accepting a compile-time boolean literal. +pub const QODEC_PARAMETER_BOOLEAN: u8 = 3; +/// `QodecParameter.kind`: a parameter accepting a compile-time string literal. +pub const QODEC_PARAMETER_STRING: u8 = 4; +/// `QodecParameter.kind`: a parameter accepting a compile-time Pauli literal. +pub const QODEC_PARAMETER_PAULI: u8 = 5; + +/// One term of a parity equation, in fully expanded form. +/// +/// `tag` selects the reference kind. `boundary`, `property` and `entry` are +/// meaningful only for `QODEC_REFERENCE_ENCODING_PROPERTY` and are zero +/// otherwise. Every index is relative to the containing gadget. +/// +/// Selectors are already expanded: an authored `circuit.readouts[0,2,5]` is +/// three references. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct QodecReference { + /// Which reference shape this is; one of the `QODEC_REFERENCE_*` values. + pub tag: u8, + /// For an encoding property, one of the `QODEC_BOUNDARY_*` values. + pub boundary: u8, + /// For an encoding property, one of the `QODEC_PROPERTY_*` values. + pub property: u8, + /// For an encoding property, the zero-based encoding position in the + /// gadget's `inputs` or `outputs`, selected by `boundary`. + pub entry: u64, + /// Zero-based circuit or gadget readout position; for an encoding property, + /// the operator position in the encoding code's `stabilizers`, `x` or `z`. + /// For `QODEC_REFERENCE_CONSTANT`, the literal bit 0 or 1. + pub index: u64, +} + +/// A list of parity equations, in compressed-sparse-row form. +/// +/// Equation `equation_index` uses the borrowed range +/// `references[offsets[equation_index] .. offsets[equation_index + 1]]`. +/// This storage is shared by checks, readouts, and frames; only checks assert zero parity. +/// Do not read offsets when `count == 0` or references when `total == 0`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecParity { + /// How many equations there are. + pub count: usize, + /// `count + 1` offsets delimiting `references`; null when `count` is zero. + pub offsets: *const usize, + /// The equations' references, concatenated. + pub references: *const QodecReference, + /// Total number of references. Zero when `count == 0`; otherwise `offsets[count]`. + pub total: usize, +} + +/// A list of strings, in compressed-sparse-row form. +/// +/// For `string_index < count`, `bytes + offsets[string_index]` is a borrowed, +/// NUL-terminated UTF-8 string. Offsets include the terminators. A string +/// containing NUL fails C projection. When `count == 0`, do not read either buffer. +/// +/// ```c +/// const QodecStrings *stabilizers = &encoding->code.stabilizers; +/// for (size_t string_index = 0; string_index < stabilizers->count; ++string_index) { +/// puts(stabilizers->bytes + stabilizers->offsets[string_index]); +/// } +/// ``` +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecStrings { + /// How many strings there are. + pub count: usize, + /// `count + 1` offsets into `bytes`; null when `count` is zero. + pub offsets: *const usize, + /// The strings, concatenated, each NUL-terminated; null when `count` is zero. + pub bytes: *const c_char, + /// Total length of `bytes` including every terminator. + pub total: usize, +} + +/// A borrowed run of numeric circuit-qubit identifiers, not array positions. +/// +/// Identifiers are `uint64_t` wherever they appear, including +/// `QodecArgumentValue.qubit.index`; counts are `size_t`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecIndices { + /// How many indices there are. + pub count: usize, + /// The indices; null when `count` is zero. + pub items: *const u64, +} + +/// A block type declared by an instruction set. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecBlock { + /// The block type's name, as instruction operands reference it. + pub name: *const c_char, + /// How many logical qubits a block of this type encodes. + pub encodes: usize, +} + +/// A borrowed run of block declarations. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecBlocks { + /// How many block declarations there are. + pub count: usize, + /// The declarations; null when `count` is zero. + pub items: *const QodecBlock, +} + +/// One positional block operand in an instruction's `in:` / `out:` list. +/// +/// Operands are nameless: position fixes the contiguous range the entry +/// occupies in the flat index space the action addresses. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecBlockOperand { + /// The block type this entry's qubits are encoded in. + pub block: *const c_char, + /// Whether the entry is variadic (`[block]` on disk). + pub is_variadic: bool, +} + +/// A borrowed run of block operands. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecBlockOperands { + /// How many operands there are. + pub count: usize, + /// The operands; null when `count` is zero. + pub items: *const QodecBlockOperand, +} + +/// A declared classical input to an instruction. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecParameter { + /// The parameter's name, as call sites and conditions reference it. + pub name: *const c_char, + /// Its declared parameter type; one of the `QODEC_PARAMETER_*` values. + /// `BIT` is the runtime, condition-eligible type; the rest accept compile-time + /// literals. This is not a `QodecArgumentValue` tag. + pub kind: u8, +} + +/// A borrowed run of parameter declarations. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecParameters { + /// How many parameters there are. + pub count: usize, + /// The parameters; null when `count` is zero. + pub items: *const QodecParameter, +} + +/// One operation in an instruction's formal semantics. +/// +/// In C, switch on `tag` and read only its matching union member. For example, +/// `QodecAction_Observe` selects `observe.observables`. +#[repr(C, u8)] +#[derive(Debug, Clone, Copy)] +pub enum QodecAction { + /// Force the state into the +1 eigenspace of every operator listed. + Stabilize { paulis: QodecStrings }, + /// A Clifford unitary, as the tableau mapping `from[i]` to `to[i]`. + Clifford { from: QodecStrings, to: QodecStrings }, + /// Apply a Pauli unitary. + Pauli { pauli: *const c_char }, + /// Measure each observable, one classical bit per entry, in order. + Observe { observables: QodecStrings }, + /// Rotate about `axis` by a literal angle or a referenced parameter's value. + Rotate { + axis: *const c_char, + /// Whether the angle is a literal rather than a parameter reference. + angle_is_literal: bool, + /// The angle, when `angle_is_literal`. + angle_literal: f64, + /// Parameter name when `angle_is_literal` is false; null for a literal. + /// `angle_operand` is the ABI field name for this parameter reference. + angle_operand: *const c_char, + }, +} + +/// One step of an instruction's action, with its optional guard. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecActionStep { + /// The operation this step performs. + pub action: QodecAction, + /// Whether the step is guarded. + pub has_condition: bool, + /// The bits XOR-ed to form the guard, when `has_condition`. + pub condition_predicates: QodecStrings, + /// Whether the guard is inverted (`unless` rather than `if`), when `has_condition`. + pub condition_invert: bool, +} + +/// A borrowed run of action steps. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecActionSteps { + /// How many steps there are. + pub count: usize, + /// The steps; null when `count` is zero. + pub items: *const QodecActionStep, +} + +/// One instruction declared by an instruction set. +/// +/// `QodecAction_Observe` steps declare measurement outcomes in action order. +/// `flags` declares additional named output bits. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecInstruction { + /// The instruction's mnemonic. + pub mnemonic: *const c_char, + /// Free-text description, or the empty string. + pub description: *const c_char, + /// Input block operands (logical qubits consumed or passed through). + pub inputs: QodecBlockOperands, + /// Output block operands (logical qubits produced or passed through). + pub outputs: QodecBlockOperands, + /// Named classical outputs following the action's measurement outcomes. + pub flags: QodecStrings, + /// Declared classical inputs; calls supply arguments for these parameters. + pub parameters: QodecParameters, + /// The formal semantics: an ordered list of guarded operations. + pub action: QodecActionSteps, + /// Free-form annotations as JSON object text, including `{}` when empty. + pub metadata_json: *const c_char, +} + +/// A borrowed run of instructions, in declaration order. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecInstructions { + /// How many instructions there are. + pub count: usize, + /// The instructions; null when `count` is zero. + pub items: *const QodecInstruction, +} + +/// A quantum error-correcting code. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecCode { + /// The code's name. + pub name: *const c_char, + /// Free-text description, or the empty string. + pub description: *const c_char, + /// Stabilizer generators, as Pauli strings. + pub stabilizers: QodecStrings, + /// Logical X operators, one per logical qubit. + pub x: QodecStrings, + /// Logical Z operators, one per logical qubit, aligned with `x`. + pub z: QodecStrings, + /// Number of logical qubits, equal to `x.count`. A valid code declares as + /// many logical Z operators, but a draft need not: bound `z` by `z.count`. + pub logical_count: usize, + /// One more than the highest qubit index across stabilizers and logical + /// operators, or zero when none is used. + pub physical_qubit_count: u64, + /// Free-form annotations as JSON object text, including `{}` when empty. + pub metadata_json: *const c_char, +} + +/// One boundary encoding: the code a gadget operand is encoded in, and where +/// that code's blocks land in the circuit. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecEncoding { + /// The code this encoding uses. + pub code: QodecCode, + /// Circuit-operand labels in code-block order, such as `"0"` or `"ancilla"`. + /// These are labels, not positions in a projected array. + pub support: QodecStrings, + /// Block-type names parallel to `support`, or an empty list if unavailable. + pub block_types: QodecStrings, +} + +/// A borrowed run of encodings. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecEncodings { + /// How many encodings there are. + pub count: usize, + /// The encodings; null when `count` is zero. + pub items: *const QodecEncoding, +} + +/// A block label or an argument value supplied at a call site. +/// +/// Used by both `QodecInstructionCall.operands` and +/// `QodecInstructionCall.arguments`. The tag describes the supplied +/// representation, not the declared `QodecParameter.kind`. +/// +/// In C, switch on `tag` and read only its matching union member. For example, +/// `QodecArgumentValue_Qubit` selects `qubit.index`, not `integer.value`. +#[repr(C, u8)] +#[derive(Debug, Clone, Copy)] +pub enum QodecArgumentValue { + /// A numeric block label or qubit identifier, not a position in a projected array. + Qubit { index: u64 }, + /// A list of numeric circuit-qubit identifiers. + QubitList { qubits: QodecIndices }, + /// An integer literal. + Integer { value: i64 }, + /// A real literal. + Number { value: f64 }, + /// A block label carried as text, or a string-valued argument. + Text { value: *const c_char }, + /// A list of string literals. + StringList { strings: QodecStrings }, + /// A prior measurement-record bit at an absolute, zero-based position in + /// measurement order across preceding calls. + Readout { index: u64 }, + /// A Boolean literal, distinct from integer literals 0 and 1. + Boolean { value: bool }, +} + +/// One block operand or parameter argument at a call site. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecArgument { + /// The parameter name for a named argument; null for a positional block operand. + pub name: *const c_char, + /// The block label or supplied argument value. + pub value: QodecArgumentValue, +} + +/// A borrowed run of block operands or parameter arguments. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecArguments { + /// How many entries there are. + pub count: usize, + /// The entries; null when `count` is zero. + pub items: *const QodecArgument, +} + +/// One constraint in a `select` pattern: a flag bit and its expected value. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecSelectConstraint { + /// The call's flag, named or addressed by its zero-based `flags[]` position. + pub flag: *const c_char, + /// The value it is expected to take, `0` or `1`. + pub bit: u8, +} + +/// Selection patterns over a call's own flags, in compressed-sparse-row form. +/// +/// Pattern `pattern_index` uses the borrowed range +/// `constraints[offsets[pattern_index] .. offsets[pattern_index + 1]]`. +/// A pattern matches when every constraint matches; selection matches when any +/// pattern matches. `count == 0` imposes no constraint. +/// Do not read offsets when `count == 0` or constraints when `total == 0`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecSelect { + /// How many patterns there are. + pub count: usize, + /// `count + 1` offsets delimiting `constraints`; null when `count` is zero. + pub offsets: *const usize, + /// The patterns' constraints, concatenated. + pub constraints: *const QodecSelectConstraint, + /// Total number of constraints. Zero when `count == 0`; otherwise `offsets[count]`. + pub total: usize, +} + +/// One invocation of a `QodecInstruction` in a circuit. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecInstructionCall { + /// The mnemonic invoked, naming an instruction in the circuit's instruction set. + pub mnemonic: *const c_char, + /// Blocks in the instruction's declared operand order. Each `name` is null; + /// its value is a numeric label (`Qubit`) or a named label (`Text`). + pub operands: QodecArguments, + /// Arguments supplied to the instruction's declared parameters, each carrying + /// the parameter name in `name`. + pub arguments: QodecArguments, + /// Selection patterns over this call's flags; empty when no selection is specified. + pub select: QodecSelect, +} + +/// A borrowed run of instruction calls, in program order. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecInstructionCalls { + /// How many calls there are. + pub count: usize, + /// The calls; null when `count` is zero. + pub items: *const QodecInstructionCall, +} + +/// A gadget's circuit: the program that runs, and the instruction set it calls into. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecCircuit { + /// The name of the target instruction set the source calls into. + pub instruction_set_name: *const c_char, + /// Inlined program text, preserved verbatim. NUL bytes fail C projection. + pub source: *const c_char, + /// The resolved source-format tag, or null when inferred from the text. + pub format: *const c_char, + /// The format selected for parsing: `format` when non-null, otherwise + /// inferred from the text. This does not guarantee a parser is available. + pub effective_format: *const c_char, + /// Calls parsed and checked against the target instruction set during `qodec_load`. + /// Empty on a parse error, an undeclared instruction, or a program with no + /// calls. Check `error` to distinguish failure from an empty program. + pub calls: QodecInstructionCalls, + /// Parse or instruction-check error, or null on success. This error leaves + /// `calls` empty without failing `qodec_load` and is not recorded in + /// `qodec_last_error`. + pub error: *const c_char, +} + +/// One gadget: the per-instruction rule lowering it to the layer below. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecGadget { + /// The instruction this gadget declares it implements. + pub implements: QodecInstruction, + /// The circuit supplied for this implementation. + pub circuit: QodecCircuit, + /// Declared zero-parity checks. Loading does not verify them against the circuit. + pub checks: QodecParity, + /// Readout equations in output order: the instruction's `observe` outcomes + /// first, then its flags. These are not all zero-parity checks. + pub readouts: QodecParity, + /// One name per readout, parallel to `readouts`; an empty string for an + /// anonymous readout. Names are labels, not reference indices. + pub readout_names: QodecStrings, + /// Number of `observe` outcomes declared by the implemented instruction. + /// For `readout_index < readouts.count`, `readout_index >= observe_count` + /// identifies a flag. An incomplete draft may declare fewer readouts than + /// outcomes, so this can exceed `readouts.count`. + pub observe_count: usize, + /// Instruction parameter names forwarded into the circuit source, parallel + /// to `parameter_targets`. + pub parameter_names: QodecStrings, + /// Circuit-source parameter names, parallel to `parameter_names`, without + /// the on-disk `circuit.source.` prefix. + pub parameter_targets: QodecStrings, + /// Input boundary encodings. An encoding-property reference selects an + /// entry here when its `boundary` is `QODEC_BOUNDARY_IN`. + pub inputs: QodecEncodings, + /// Output boundary encodings, indexed likewise for `QODEC_BOUNDARY_OUT`. + pub outputs: QodecEncodings, + /// Free-form annotations as JSON object text, including `{}` when empty. + pub metadata_json: *const c_char, + /// Output logical-sign reference paths in sorted order, parallel to `frames`. + pub frame_targets: QodecStrings, + /// Additional output-sign corrections as XOR equations. Empty entries apply + /// no correction. These are definitions, not zero-parity checks. + pub frames: QodecParity, +} + +/// A borrowed run of gadgets, in sorted mnemonic order. +/// Ordering uses UTF-8 bytes. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecGadgets { + /// How many gadgets there are. + pub count: usize, + /// The gadgets; null when `count` is zero. + pub items: *const QodecGadget, +} + +/// One layer of the lowering chain: an instruction set plus the gadgets lowering it. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecLayer { + /// The name of this layer's instruction set. + pub instruction_set_name: *const c_char, + /// The instruction set's description, or the empty string. + pub instruction_set_description: *const c_char, + /// The block types the instruction set declares. + pub blocks: QodecBlocks, + /// The instruction set's instructions, in declaration order. + pub instructions: QodecInstructions, + /// The gadgets lowering this layer to the next. Empty on the bottom layer, + /// which is the target of the layer above and lowers no further. + pub gadgets: QodecGadgets, + /// The instruction set's free-form annotations as JSON object text, including `{}` when empty. + pub instruction_set_metadata_json: *const c_char, +} + +/// A borrowed run of layers, ordered logical to physical. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QodecLayers { + /// How many layers there are. + pub count: usize, + /// The layers; null when `count` is zero. + pub items: *const QodecLayer, +} + +/// The root of a loaded qodec. +/// +/// Produced by `qodec_load()` and released by `qodec_unload()`. Treat this struct +/// and all reachable storage as read-only. All nested pointers borrow from +/// this root and become invalid when it is unloaded. +/// +/// Pass the original root pointer to `qodec_unload` exactly once, not a copy of +/// the struct. Never free the root or its nested storage yourself. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct Qodec { + /// The qodec's name, or the empty string when the manifest has none. + pub name: *const c_char, + /// The manifest description, or the empty string. + pub description: *const c_char, + /// Whether the manifest declared an on-disk schema version. + pub has_schema_version: bool, + /// The declared on-disk schema version; meaningful only when `has_schema_version`. + pub schema_version: u32, + /// The lowering chain, ordered logical to physical. + pub layers: QodecLayers, + /// The manifest's free-form annotations as JSON object text, including `{}` when empty. + pub metadata_json: *const c_char, +} + +impl QodecReference { + fn from_index(target: ReferenceTarget, index: usize) -> Self { + match target { + ReferenceTarget::CircuitReadout => Self { + tag: QODEC_REFERENCE_CIRCUIT_READOUT, + boundary: 0, + property: 0, + entry: 0, + index: index as u64, + }, + ReferenceTarget::Readout => Self { + tag: QODEC_REFERENCE_READOUT, + boundary: 0, + property: 0, + entry: 0, + index: index as u64, + }, + ReferenceTarget::EncodingProperty { + boundary, + entry, + property, + } => Self { + tag: QODEC_REFERENCE_ENCODING_PROPERTY, + boundary: match boundary { + GadgetBoundary::In => QODEC_BOUNDARY_IN, + GadgetBoundary::Out => QODEC_BOUNDARY_OUT, + }, + property: match property { + EncodingPropertyKind::Stabilizer => QODEC_PROPERTY_STABILIZER, + EncodingPropertyKind::LogicalX => QODEC_PROPERTY_LOGICAL_X, + EncodingPropertyKind::LogicalZ => QODEC_PROPERTY_LOGICAL_Z, + }, + entry: entry as u64, + index: index as u64, + }, + } + } +} + +// ── The C-ready projection ─────────────────────────────────────────────────── +// +// qodec stores parity equations as parsed expressions, names as Rust `String`s and +// collections as `Vec`/`BTreeMap`, none of which C can traverse. Building a +// projection once at open is what lets the whole tree be plain structs the +// caller neither allocates nor frees. +// +// Everything is interned through `Arena`, which boxes each buffer before +// handing back a pointer into it. Boxed buffers and `Vec` heap storage do not +// move when the vectors holding them grow, so pointers taken during +// construction stay valid for the life of the allocation. + +/// A parity section, flattened to CSR. +struct Parity { + offsets: Vec, + references: Vec, +} + +impl Parity { + /// Expand cached selectors in equation order without parsing or omitting terms. + fn build<'a>(equations: impl IntoIterator>) -> Self { + let mut offsets = vec![0_usize]; + let mut references = Vec::new(); + for equation in equations { + for atom in equation { + match atom { + ParityTerm::Reference(reference) => references.extend( + reference + .indices() + .map(|index| QodecReference::from_index(reference.target(), index)), + ), + ParityTerm::Bit(value) => references.push(QodecReference { + tag: QODEC_REFERENCE_CONSTANT, + boundary: 0, + property: 0, + entry: 0, + index: u64::from(*value), + }), + } + } + offsets.push(references.len()); + } + Self { offsets, references } + } + + fn view(&self) -> QodecParity { + if self.offsets.len() <= 1 { + return QodecParity { + count: 0, + offsets: std::ptr::null(), + references: std::ptr::null(), + total: 0, + }; + } + QodecParity { + count: self.offsets.len() - 1, + offsets: self.offsets.as_ptr(), + references: self.references.as_ptr(), + total: self.references.len(), + } + } +} + +#[cfg(test)] +mod parity_tests { + use super::*; + use qodec::Reference; + + #[test] + fn cached_selectors_preserve_equation_boundaries_order_and_duplicates() { + let equations = [ + vec![ + Reference::parse("out[01].z[3, 1,3]").unwrap().into(), + Reference::parse("circuit.readouts[00:03:2]").unwrap().into(), + ], + vec![], + vec![ + Reference::parse("readouts[02]").unwrap().into(), + ParityTerm::Bit(false), + ParityTerm::Bit(true), + ], + ]; + let parity = Parity::build(&equations); + assert_eq!(parity.offsets, [0, 5, 5, 8]); + assert_eq!( + parity.references.iter().map(|term| term.index).collect::>(), + [3, 1, 3, 0, 2, 2, 0, 1] + ); + for term in &parity.references[..3] { + assert_eq!(term.tag, QODEC_REFERENCE_ENCODING_PROPERTY); + assert_eq!(term.boundary, QODEC_BOUNDARY_OUT); + assert_eq!(term.entry, 1); + assert_eq!(term.property, QODEC_PROPERTY_LOGICAL_Z); + } + assert_eq!(parity.references[3].tag, QODEC_REFERENCE_CIRCUIT_READOUT); + assert_eq!(parity.references[5].tag, QODEC_REFERENCE_READOUT); + assert_eq!(parity.references[6].tag, QODEC_REFERENCE_CONSTANT); + assert_eq!(parity.references[7].tag, QODEC_REFERENCE_CONSTANT); + } +} + +/// A string list, flattened to CSR with each entry NUL-terminated in place. +struct Strings { + offsets: Vec, + bytes: Vec, +} + +impl Strings { + fn build<'a>(values: impl IntoIterator) -> Result { + let mut offsets = Vec::new(); + let mut bytes = Vec::new(); + for value in values { + let value = CString::new(value)?; + offsets.push(bytes.len()); + bytes.extend_from_slice(value.as_bytes_with_nul()); + } + offsets.push(bytes.len()); + Ok(Self { offsets, bytes }) + } + + fn view(&self) -> QodecStrings { + if self.offsets.len() <= 1 { + return QodecStrings { + count: 0, + offsets: std::ptr::null(), + bytes: std::ptr::null(), + total: 0, + }; + } + QodecStrings { + count: self.offsets.len() - 1, + offsets: self.offsets.as_ptr(), + bytes: self.bytes.as_ptr().cast::(), + total: self.bytes.len(), + } + } +} + +/// One `select` expectation, flattened to CSR. +struct Select { + offsets: Vec, + constraints: Vec, +} + +/// Owns every buffer the projected tree points into. +/// +/// Each `intern_*` builds a buffer, stores it, and returns a view of it. Every +/// stored type keeps its data in its own heap allocation — `CString`, and the +/// `Vec`s inside `Strings`, `Parity` and `Select` — and the views point only at +/// those, so later interning can move the structs without invalidating +/// anything. +#[derive(Default)] +struct Arena { + cstrings: Vec, + strings: Vec, + parities: Vec, + selects: Vec