feat(stim-parser): native R_X/R_Y/R_Z/U3 rotation gates + require *pi in tags#159
feat(stim-parser): native R_X/R_Y/R_Z/U3 rotation gates + require *pi in tags#159david-pl wants to merge 9 commits into
Conversation
clifft/tsim circuits use bare rotation mnemonics (`R_Z(0.02) 0`, `U3(a,b,c) 0`) that the parser previously rejected as unknown instructions; only the tagged `I[R_Z(theta=..*pi)]` / `I[U3(...)]` forms were accepted (via tsim's shorthand_to_stim rewrite). Register R_X/R_Y/R_Z (Exact(1)) and U3 (Exact(3)) in the instruction table and lower them to the existing Rotation/U3 extended variants. The bare argument is in half-turns (clifft convention: clifft's `R_Z(a) = exp(-i*a*pi/2*Z)`, gate_data.h half-turn units), so the lowering multiplies by pi to obtain the radians theta the Rotation variant carries — making `R_Z(a)` identical to `I[R_Z(theta=a*pi)]`. Verified empirically: clifft and ppvm rotation statistics match to sampling noise across several angles. Tags are intentionally untouched here (a follow-up tightens them). ppvm-stim's exhaustive GateName matches gain the new variants: they are always lowered away before execution, so they join T/T_DAG as Ok-in-validate / unreachable-in-executor.
Mirror tsim's parametric-tag convention: rotation and U3 tag angles
must be written in half-turns as `<n>*pi` (e.g. `I[R_Z(theta=0.5*pi)]`),
and a bare number (`I[R_Z(theta=0.5)]`) is now rejected with an
`invalid-tag` diagnostic. tsim's tag parser requires the `*pi` literal
(`core/parse.py`, regex `^\w+=<float>\*pi$`) and treats the coefficient
as half-turns; ppvm now refuses the ambiguous bare form rather than
silently reading it as radians.
Mechanics:
- `TagParam::Named` gains a `had_pi` flag, captured by a new
`pi_expr_flagged` grammar rule (pi_expr is now defined in terms of it).
- `exact_named_params` rejects any rotation/U3 angle written without `*pi`.
- The printer re-emits the half-turn form (`theta={theta/pi}*pi`); since
every parser-produced rotation angle is a `<n>*pi` multiple, theta/pi
recovers the coefficient exactly (verified for the relevant float set),
so print -> parse stays a fixpoint.
The bare *gate* form `R_Z(0.5)` (clifft half-turn shorthand) is
unchanged — only the bracketed tag form is tightened, matching tsim
(shorthand `R_Z(a)` vs canonical tag `theta=a*pi`).
Tests across extended/roundtrip/proptest suites move to the `*pi` form;
the proptest AST generator now produces `coeff*pi` angles (the only
parser-producible shape). The ppvm-python fixpoint test is updated too.
|
FYI @rafaelha @Roger-luo: adds some additional non-Clifford instructions that tsim and clifft support. With this, we support everything but TPP and SPP, I think. |
|
There was a problem hiding this comment.
Pull request overview
This PR extends the stim-parser → ppvm-stim pipeline to natively accept clifft/tsim-style parameterized rotation mnemonics (R_X, R_Y, R_Z, U3) and tightens the extended-tag syntax so rotation/U3 tag angles must explicitly carry a pi unit (preventing ambiguity between half-turns and radians).
Changes:
- Adds
R_X/R_Y/R_Z/U3to the instruction table and lowers their arguments (half-turns) into existing extendedRotation/U3instructions (radians). - Updates the tag grammar/AST to track whether
piappeared, and rejects rotation/U3 tags written without*pi-style units. - Updates printing and test corpora/property tests to use the canonical
...*piform.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ppvm-python/test/test_stim_api.py | Updates Python-side Stim API test input to the new canonical ...*pi tag form. |
| crates/stim-parser/tests/tags.rs | Adjusts tag parsing tests for the new had_pi field. |
| crates/stim-parser/tests/roundtrip.rs | Updates roundtrip corpus and expected printed forms to include *pi. |
| crates/stim-parser/tests/proptest_roundtrip.rs | Updates generated extended-tag fragments to use *pi. |
| crates/stim-parser/tests/proptest_ast.rs | Adjusts AST generators so Rotation/U3 angles originate from coefficient * PI. |
| crates/stim-parser/tests/extended.rs | Updates extended parsing/lowering tests to assert radians (coefficient * PI). |
| crates/stim-parser/src/syntax/grammar.rs | Introduces pi_expr_flagged and records had_pi on named tag params. |
| crates/stim-parser/src/print/mod.rs | Re-emits rotation/U3 tags in ...*pi form and prints named params respecting had_pi. |
| crates/stim-parser/src/pipeline/validate.rs | Updates validation test scaffolding for the had_pi field. |
| crates/stim-parser/src/pipeline/lower.rs | Lowers bare R_*/U3 mnemonics in half-turns; enforces had_pi for rotation/U3 tags. |
| crates/stim-parser/src/instructions/mod.rs | Adds new GateName variants and table entries for R_* and U3. |
| crates/stim-parser/src/ast/shared.rs | Extends TagParam::Named to include had_pi. |
| crates/ppvm-stim/tests/executor.rs | Updates executor test circuit to use *pi for all U3 parameters. |
| crates/ppvm-stim/src/validate.rs | Treats new gate names as valid-at-validate (they should be lowered before execution). |
| crates/ppvm-stim/src/executor.rs | Marks new gate names as unreachable!() in the executor since they are expected to be lowered away. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The bare-rotation lowering arms bound `tags` but never used them, so a tag on a bare rotation mnemonic (e.g. `R_Z[foo](0.5)`) parsed and was silently dropped — lossy on parse->lower->print and able to mask a typo'd tag. A tag has no meaning on the bare mnemonics, so reject a non-empty tag list with an `invalid-tag` diagnostic pointing at the argument form. (The pre-existing T/T_DAG arms drop tags the same way; left untouched here as out of scope for this change.)
Follow-up to the bare R_X/R_Y/R_Z/U3 tag fix: the native T / T_DAG arms dropped any tag the same way (e.g. `T[foo] 0` parsed and silently lost `[foo]`). Reject a non-empty tag list with an `invalid-tag` diagnostic that points at the canonical tagged spelling, S[T] / S_DAG[T].
| TagParam::Named { key, value, had_pi } => { | ||
| if *had_pi { | ||
| write!(out, "{key}={}*pi", FloatLit(*value / std::f64::consts::PI))?; | ||
| } else { | ||
| write!(out, "{key}={}", FloatLit(*value))?; | ||
| } |
…ouble-scale) Bare rotation/U3 args are in half-turns and get multiplied by pi when lowering (R_Z(0.5) == I[R_Z(theta=0.5*pi)]). But args flow through the generic pi_expr parser, so R_Z(0.5*pi) / R_Z(pi) were silently scaled by pi twice (0.5*pi*pi instead of 0.5*pi) with no diagnostic — the inverse of the ambiguity the *pi tag requirement was added to prevent. args_block now carries a had_pi flag (via the existing pi_expr_flagged); the validator rejects a *pi argument on the bare R_X/R_Y/R_Z/U3 mnemonics with a clear half-turn-arg error. pi-expressions remain valid in args for every other instruction (stim.rs compatibility, e.g. X_ERROR(0.5*pi)). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review: edge cases + a pushed fixReviewed the PR for unhandled edge cases. Found one confirmed correctness bug and pushed a fix to this branch ( 🔴 Fixed in
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…ail) The rotation/U3 printer emitted `theta/PI` directly, so the division's rounding leaked into the output: `theta=0.76*pi` printed as `theta=0.7599999999999999*pi`. Roughly 10% of ordinary decimal angles were affected. `pi_coeff` now returns the shortest decimal `c` whose `c * PI` recovers the stored radians bit-for-bit, falling back to `value / PI` when no exact short form exists. Because acceptance requires exact equality, this both prints cleanly and keeps `parse → print` lossless and the printer a byte-for-byte fixpoint — verified by a new property test over arbitrary decimal coefficients (exact theta recovery + fixpoint + no rounding tail) plus a unit test covering tags, bare gates, and U3. Addresses Copilot's print/mod.rs review note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addressed Copilot's printer note (
|
Summary
Lets
stim-parser/ppvm-stimnatively ingest the clifft/tsim rotationvocabulary, so circuits using
R_X/R_Y/R_Z/U3no longer need tsim'sshorthand_to_stimrewrite before ppvm can run them.Two commits:
1.
feat(stim-parser): parse bare R_X/R_Y/R_Z/U3 rotation gatesR_X/R_Y/R_Z(Exact(1)) andU3(Exact(3)) in theinstruction table; lower them to the existing
Rotation/U3extendedvariants. No executor/validator changes needed downstream.
R_Z(a) = exp(-i*a*pi/2*Z),gate_data.h/frontend.cc), so loweringmultiplies by π — making
R_Z(a)identical toI[R_Z(theta=a*pi)].Verified empirically: clifft and ppvm rotation statistics match to
sampling noise across several angles.
GateNamematches gain the new variants; theyalways lower away before execution, so they join
T/T_DAGasOk-in-validate /unreachable!-in-executor.2.
feat(stim-parser)!: require *pi in rotation/U3 tag angles<n>*pi(I[R_Z(theta=0.5*pi)]); a bare number (theta=0.5) is nowrejected with
invalid-tag. tsim's tag parser requires the*piliteraland reads the coefficient as half-turns; ppvm refuses the ambiguous bare
form rather than silently treating it as radians.
TagParam::Namedgains ahad_piflag (newpi_expr_flaggedgrammarrule);
exact_named_paramsenforces it; the printer re-emits*pi(
theta={theta/pi}*pi) so print→parse stays a fixpoint.R_Z(0.5)is unchanged — only the bracketed tag istightened, matching tsim (shorthand
R_Z(a)vs canonicaltheta=a*pi).Background
clifft uses half-turn units; tsim's canonical tag is
theta=a*pi(itstores the coefficient as an exact
Fraction). ppvm's tag stored radiansand was the outlier — it coincided with the ecosystem only on the
*piform. These commits make the bare mnemonic clifft-faithful and tighten the
tag to the
*piconvention.Test plan
cargo test -p stim-parser -p ppvm-stim— all 25 test binaries pass.cargo build --workspaceclean.Notes
ppvm-pythonfixpoint test was updated to the*piform. It onlyruns against the compiled wheel, which still ships old
ppvm 0.1.0— thefeature reaches Python only after
ppvm-python-nativeis rebuilt withmaturin (not done here).
SPP[T]/SPP_DAG[T](Pauli-product rotation, fromTPP) remainunsupported and out of scope.
🤖 Generated with Claude Code