Skip to content

Validate PHIR JSON quantum declarations through one shared rule at every entry point - #892

Merged
ciaranra merged 1 commit into
devfrom
phir-json-declaration-validation
Sep 28, 2026
Merged

ciaranra merged 1 commit into
devfrom
phir-json-declaration-validation

Conversation

@ciaranra

Copy link
Copy Markdown
Member

Closes #886.

Problem

PECOS's PHIR JSON entry points disagreed about what a legal quantum declaration is, in opposite directions. Verified on dev at bb5932b06:

Input phir_json_to_module serde_json::from_str::<PHIRProgram>
{"data":"qvar_define","variable":"q","size":2} (no data_type) Ok Err "missing data_type"
{"data":"qvar_define","data_type":"u32","variable":"q","size":2} Err Ok

The v0.1 specification marks data_type optional and size required (specification/v0.1/spec.md:141), and the upstream phir.model.QVarDefine agrees independently: data_type: str | None, size: int with Gt(gt=0). So row 1 is a legal program one door rejected, and row 2 an illegal program the other accepted. The fix in #887 had only covered one of the two doors.

Two more divergences, found by reading:

  • classical_interpreter.rs matched "qvar_define" if data_type == "qubits", so a declaration with any other type fell through a catch-all and was silently ignored — the register never existed, and later arguments naming it failed or mis-resolved.
  • The same site called infer_size, which scrapes digits out of the type name, so a quantum declaration with no size silently became size 0.

Change

One validate_quantum_declaration beside infer_size in ast.rs states the rules once: an absent data_type means "qubits"; a present value that is not "qubits" errors naming the register and the value; size is required and must be positive; overflow errors. A sibling declaration_size routes quantum declarations through it and leaves classical inference untouched, so a quantum size is never inferred.

Every entry point calls it: the AST deserializer, the converter pre-scan and emission, the Rust interpreter, the processor's declaration handler, the engine's constructors and command execution, and the block executor. pyphir.py takes the minimal local fix — default an absent type, keep its existing wrong-type rejection — with a comment naming the Rust validator as the normative statement rather than delegating across the language boundary.

Duplicate policy stays deliberately non-uniform, documented on the validator: OperationProcessor::add_quantum_variable tolerates an identical redeclaration because the engine visits declarations both when loading the header and during execution, while rejecting a conflicting one; the converter and interpreter see each op once and reject any duplicate. Python's overwrite behaviour is left as it was.

Also closed: an Operation classification path where a declaration carrying a variables key was parsed as a data export, bypassing declaration validation entirely.

Agreement after the change

Input Converter AST Rust interpreter Engine Python reader
Type omitted, positive size Accept Accept Accept Accept Accept
"qubits", positive size Accept Accept Accept Accept Accept
"u32" on a quantum declaration Reject Reject Reject Reject Reject
Quantum size absent Reject Reject Reject Reject Reject
Quantum size zero Reject Reject Reject Reject Reject
Classical type absent Reject Reject Reject Reject Reject

Tests

tests/quantum_declarations.rs plus tests/pecos/unit/test_quantum_declarations.py: the verdict table above asserted entry point by entry point, declaration-order ids under an omitted type, the processor's duplicate policy both ways, converter and interpreter rejecting duplicates, constructed ASTs unable to bypass validation, and size overflow.

Mutation-checked: accepting a present wrong type, and inferring a quantum size instead of erroring, each fail declaration_verdicts and constructed_ast_cannot_bypass_validation; rejecting an identical redeclaration fails the processor policy test.

Verification

cargo clippy --locked --workspace --all-targets --all-features -- -D warnings (the lane CI runs), cargo test -p pecos-phir-json -p pecos-phir -p pecos-qasm --no-fail-fast (990 passed), cargo fmt --check, just python-ci-build-test, just pytest-ci-core-shard rest (6743 passed), just python-ci-lint. Branch is level with dev.

How this was produced

Implemented by OpenAI gpt-6-astra (Codex CLI 0.154.0) from a task packet written by Claude Fable 5.1 in Claude Code. The first attempt reached the contract by filtering quantum declarations out of upstream PhirModel.model_validate, on the premise that the external schema "incorrectly" required a positive size; I read QVarDefine and found the schema correct, so that was reverted along with a new pyo3 validation binding and an unrequested change to Python's duplicate handling, and the contract was corrected to reject zero instead. Claude reviewed every hunk, re-ran the named lanes, and repeated the mutation checks independently — including discarding one of its own mutations that failed to compile and therefore proved nothing. Posted at the maintainer's request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pecos-phir-json quantum declaration validation differs between converter, interpreter and processor (missing size, duplicates, data_type)

1 participant