diff --git a/crates/ppvm-stim/tests/executor.rs b/crates/ppvm-stim/tests/executor.rs index d8030f08..dd3c25c3 100644 --- a/crates/ppvm-stim/tests/executor.rs +++ b/crates/ppvm-stim/tests/executor.rs @@ -46,10 +46,23 @@ fn rx_pi_flips_qubit() { #[test] fn u3_pi_flip_via_y_axis() { - let (results, _) = run("I[U3(theta=1.0*pi, phi=0.0, lambda=0.0)] 0\nM 0", 1); + let (results, _) = run("I[U3(theta=1.0*pi, phi=0.0*pi, lambda=0.0*pi)] 0\nM 0", 1); assert_eq!(results, vec![Some(true)]); } +#[test] +fn u3_all_angles_nonzero_exercises_phi_lambda() { + // U3(theta=pi, phi=pi/2, lambda=pi/2) == Y (clifft Rz(phi)Ry(theta)Rz(lambda)), + // so H·U3·H == H·Y·H == -Y and |0> -> |1> deterministically. The H frame makes + // the outcome sensitive to phi *and* lambda (drop or mis-scale either and P(1) + // collapses to ~0.5). Half-turn tag args 1.0/0.5/0.5 each get *pi at lowering. + let tag = run( + "H 0\nI[U3(theta=1.0*pi, phi=0.5*pi, lambda=0.5*pi)] 0\nH 0\nM 0", + 1, + ); + assert_eq!(tag.0, vec![Some(true)]); +} + #[test] fn t_gate_via_s_t_tag_no_op_on_zero() { let (results, _) = run("S[T] 0\nM 0", 1); diff --git a/crates/stim-parser/src/ast/shared.rs b/crates/stim-parser/src/ast/shared.rs index ec6c8279..b72c03f4 100644 --- a/crates/stim-parser/src/ast/shared.rs +++ b/crates/stim-parser/src/ast/shared.rs @@ -74,7 +74,14 @@ pub struct Tag { #[derive(Debug, Clone, PartialEq)] pub enum TagParam { Positional(f64), - Named { key: String, value: f64 }, + /// A `key=value` tag parameter. `had_pi` records whether the value was + /// written as a `*pi` (or bare `pi`) expression — rotation/U3 tags + /// require it (half-turn convention), and the printer re-emits `*pi`. + Named { + key: String, + value: f64, + had_pi: bool, + }, } /// The rotation axis for an extended-dialect `R_X` / `R_Y` / `R_Z` rotation. diff --git a/crates/stim-parser/src/pipeline/lower.rs b/crates/stim-parser/src/pipeline/lower.rs index 4fc5b695..7325a8b2 100644 --- a/crates/stim-parser/src/pipeline/lower.rs +++ b/crates/stim-parser/src/pipeline/lower.rs @@ -104,6 +104,15 @@ fn lower_gate( match name { // Native T / T_DAG mnemonics lower to the same sugar as `S[T]` / `S_DAG[T]`. T | TDag => { + if let Some(tag) = tags.first() { + return invalid_tag( + &tag.name, + name.canonical_name(), + span, + "bare T/T_DAG take no tags; use S[T] / S_DAG[T] for the tagged form", + sink, + ); + } let Some(targets) = qubit_targets(targets, name.canonical_name(), span, sink)? else { return Ok(None); }; @@ -445,7 +454,7 @@ fn exact_named_params( sink, ); } - TagParam::Named { key, value } => { + TagParam::Named { key, value, had_pi } => { let Some(index) = required.iter().position(|required_key| key == required_key) else { return invalid_tag( @@ -465,6 +474,17 @@ fn exact_named_params( sink, ); } + // Rotation/U3 angles are in half-turns: require the `*pi` form, + // mirroring tsim, so a bare number can't be mistaken for radians. + if !had_pi { + return invalid_tag( + &tag.name, + instruction, + span, + format!("parameter '{key}' must be written as *pi (half-turns)"), + sink, + ); + } seen[index] = true; values[index] = *value; } @@ -552,7 +572,7 @@ mod tests { #[test] fn identity_rotation_x_lowers() { - let prog = lower_extended("I[R_X(theta=0.5)] 0").expect("lower"); + let prog = lower_extended("I[R_X(theta=0.5*pi)] 0").expect("lower"); match &prog.instructions[0] { ExtendedInstruction::Rotation { axis, @@ -561,7 +581,7 @@ mod tests { .. } => { assert_eq!(*axis, Axis::X); - assert_eq!(*theta, 0.5); + assert!((*theta - 0.5 * std::f64::consts::PI).abs() < 1e-12); assert_eq!(targets, &vec![0]); } other => panic!("{other:?}"), @@ -570,7 +590,8 @@ mod tests { #[test] fn identity_u3_lowers() { - let prog = lower_extended("I[U3(theta=0.5, phi=1.0, lambda=1.5)] 0").expect("lower"); + let prog = + lower_extended("I[U3(theta=0.5*pi, phi=1.0*pi, lambda=1.5*pi)] 0").expect("lower"); match &prog.instructions[0] { ExtendedInstruction::U3 { theta, @@ -579,15 +600,55 @@ mod tests { targets, .. } => { - assert_eq!(*theta, 0.5); - assert_eq!(*phi, 1.0); - assert_eq!(*lambda, 1.5); + let pi = std::f64::consts::PI; + assert!((*theta - 0.5 * pi).abs() < 1e-12); + assert!((*phi - 1.0 * pi).abs() < 1e-12); + assert!((*lambda - 1.5 * pi).abs() < 1e-12); assert_eq!(targets, &vec![0]); } other => panic!("{other:?}"), } } + #[test] + fn bare_t_with_tag_is_rejected() { + // A tag on bare T/T_DAG is meaningless (the tagged form is S[T]); reject + // it rather than silently dropping it. + let err = lower_extended("T[foo] 0").unwrap_err(); + assert_eq!(err.last().unwrap().code, Some("invalid-tag")); + } + + #[test] + fn bare_t_dag_with_tag_is_rejected() { + let err = lower_extended("T_DAG[foo] 0").unwrap_err(); + assert_eq!(err.last().unwrap().code, Some("invalid-tag")); + } + + #[test] + fn rotation_tag_without_pi_is_rejected() { + // Mirror tsim: rotation tag angles must be written as *pi (half-turns). + let err = lower_extended("I[R_Z(theta=0.5)] 0").unwrap_err(); + assert_eq!(err.last().unwrap().code, Some("invalid-tag")); + } + + #[test] + fn u3_tag_without_pi_is_rejected() { + let err = lower_extended("I[U3(theta=0.5, phi=1.0, lambda=1.5)] 0").unwrap_err(); + assert_eq!(err.last().unwrap().code, Some("invalid-tag")); + } + + #[test] + fn rotation_tag_with_pi_is_accepted() { + let prog = lower_extended("I[R_Z(theta=0.5*pi)] 0").expect("lower"); + match &prog.instructions[0] { + ExtendedInstruction::Rotation { axis, theta, .. } => { + assert_eq!(*axis, Axis::Z); + assert!((*theta - 0.5 * std::f64::consts::PI).abs() < 1e-12); + } + other => panic!("{other:?}"), + } + } + #[test] fn i_error_loss_lowers() { let prog = lower_extended("I_ERROR[loss](0.01) 0").expect("lower"); diff --git a/crates/stim-parser/src/pipeline/validate.rs b/crates/stim-parser/src/pipeline/validate.rs index b3354e67..343ed29e 100644 --- a/crates/stim-parser/src/pipeline/validate.rs +++ b/crates/stim-parser/src/pipeline/validate.rs @@ -548,6 +548,7 @@ mod tests { TagParam::Named { key: "theta".to_string(), value: 0.25, + had_pi: false, }, ], }], @@ -559,7 +560,7 @@ mod tests { assert!(matches!(tags[0].params[0], TagParam::Positional(0.5))); assert!(matches!( &tags[0].params[1], - TagParam::Named { key, value } if key == "theta" && *value == 0.25 + TagParam::Named { key, value, .. } if key == "theta" && *value == 0.25 )); } other => panic!("{other:?}"), diff --git a/crates/stim-parser/src/print/mod.rs b/crates/stim-parser/src/print/mod.rs index 6effd7b2..411553d6 100644 --- a/crates/stim-parser/src/print/mod.rs +++ b/crates/stim-parser/src/print/mod.rs @@ -69,8 +69,12 @@ fn write_tags(out: &mut dyn fmt::Write, tags: &[Tag]) -> fmt::Result { } match p { TagParam::Positional(v) => write!(out, "{}", FloatLit(*v))?, - TagParam::Named { key, value } => { - write!(out, "{key}={}", FloatLit(*value))?; + TagParam::Named { key, value, had_pi } => { + if *had_pi { + write!(out, "{key}={}*pi", FloatLit(pi_coeff(*value)))?; + } else { + write!(out, "{key}={}", FloatLit(*value))?; + } } } } @@ -165,6 +169,37 @@ impl fmt::Display for FloatLit { } } +/// The coefficient `c` to print for a `*pi` literal carrying the radians +/// `value`. The naive `value / PI` is correct but, because the division +/// rounds, often prints a long tail (`0.76*pi` → `0.7599999999999999*pi`). +/// +/// Parser-produced angles always originate from a decimal coefficient — a +/// `*pi` rotation/U3 tag — so a short decimal `c` with +/// `c * PI == value` (bit-for-bit) exists. We return the shortest such `c`, +/// which prints cleanly *and* re-parses back to exactly `value`. Requiring +/// exact equality is what keeps `parse → print` lossless and the printer a +/// fixpoint; for any `value` with no exact short form we fall back to the +/// naive `value / PI` (same output as before). +fn pi_coeff(value: f64) -> f64 { + let pi = std::f64::consts::PI; + let q = value / pi; + if !q.is_finite() { + return q; + } + // `{:.*e}` with `prec` digits after the mantissa point is `prec + 1` + // significant digits; 17 sig-digits round-trips any f64, so by `prec = 16` + // `candidate == q` and the loop has tried every shorter rounding first. + for prec in 0..=16 { + let candidate: f64 = format!("{q:.prec$e}") + .parse() + .expect("a formatted float always re-parses"); + if candidate * pi == value { + return candidate; + } + } + q +} + // --------------------------------------------------------------------------- // StimPrint for shared *Op structs // --------------------------------------------------------------------------- @@ -291,7 +326,14 @@ impl StimPrint for ExtendedInstruction { Axis::Y => "R_Y", Axis::Z => "R_Z", }; - write!(out, "I[{}(theta={})]", axis_tag, FloatLit(*theta))?; + // theta is radians; re-emit the half-turn `*pi` form the + // rotation tags require (see exact_named_params). + write!( + out, + "I[{}(theta={}*pi)]", + axis_tag, + FloatLit(pi_coeff(*theta)) + )?; write_usize_targets(out, targets)?; } ExtendedInstruction::U3 { @@ -303,10 +345,10 @@ impl StimPrint for ExtendedInstruction { } => { write!( out, - "I[U3(theta={}, phi={}, lambda={})]", - FloatLit(*theta), - FloatLit(*phi), - FloatLit(*lambda), + "I[U3(theta={}*pi, phi={}*pi, lambda={}*pi)]", + FloatLit(pi_coeff(*theta)), + FloatLit(pi_coeff(*phi)), + FloatLit(pi_coeff(*lambda)), )?; write_usize_targets(out, targets)?; } @@ -381,9 +423,9 @@ mod tests { #[test] fn extended_printed_form_lowers_sugar_into_canonical_stim() { - let src = "S[T] 0\nI[R_X(theta=0.25)] 1\nI_ERROR[loss](0.01) 2\n"; + let src = "S[T] 0\nI[R_X(theta=0.25*pi)] 1\nI_ERROR[loss](0.01) 2\n"; let ast = parse_extended(src).unwrap(); - let expected = "S[T] 0\nI[R_X(theta=0.25)] 1\nI_ERROR[loss](0.01) 2\n"; + let expected = "S[T] 0\nI[R_X(theta=0.25*pi)] 1\nI_ERROR[loss](0.01) 2\n"; assert_eq!(ast.to_stim(), expected); } @@ -393,4 +435,32 @@ mod tests { let ast = parse("CX rec[-1] 0\nMPP X0*Y1*Z2\n").unwrap(); assert_eq!(ast.to_stim(), "CX rec[-1] 0\nMPP X0*Y1*Z2\n"); } + + #[test] + fn rotation_pi_coeff_prints_clean_and_round_trips() { + // theta is stored in radians as `c*PI`; printing `c = theta/PI` naively + // would emit a rounding tail like `0.7599999999999999*pi`. The printer + // recovers the short coefficient instead — for rotation and U3 tags — + // and `print → parse → print` stays a fixpoint. + for (src, expected) in [ + // Non-binary-friendly decimals that `theta/PI` mangles. + ("I[R_Z(theta=0.34*pi)] 0\n", "I[R_Z(theta=0.34*pi)] 0\n"), + ("I[R_Y(theta=0.76*pi)] 1\n", "I[R_Y(theta=0.76*pi)] 1\n"), + ("I[R_X(theta=-2.78*pi)] 2\n", "I[R_X(theta=-2.78*pi)] 2\n"), + ( + "I[U3(theta=0.34*pi, phi=0.91*pi, lambda=0.07*pi)] 0\n", + "I[U3(theta=0.34*pi, phi=0.91*pi, lambda=0.07*pi)] 0\n", + ), + ] { + let printed = parse_extended(src).unwrap().to_stim(); + assert_eq!(printed, expected, "first print of {src:?}"); + assert!( + !printed.contains("999999") && !printed.contains("000000"), + "coefficient printed with a rounding tail: {printed:?}" + ); + // Fixpoint: re-parsing and re-printing reproduces it byte-for-byte. + let reprinted = parse_extended(&printed).unwrap().to_stim(); + assert_eq!(reprinted, printed, "printer is not a fixpoint for {src:?}"); + } + } } diff --git a/crates/stim-parser/src/syntax/grammar.rs b/crates/stim-parser/src/syntax/grammar.rs index 9ff4dbed..d32c2203 100644 --- a/crates/stim-parser/src/syntax/grammar.rs +++ b/crates/stim-parser/src/syntax/grammar.rs @@ -82,21 +82,29 @@ pub(crate) fn signed_float<'src>() -> impl Parser<'src, &'src str, f64, Extra<'s .map(|s: &str| s.parse::().expect("validated by combinator shape")) } -/// Pi-expression: `pi`, `*pi`, or plain number. Evaluates to f64. -pub(crate) fn pi_expr<'src>() -> impl Parser<'src, &'src str, f64, Extra<'src>> + Clone { - let pi_kw = just("pi").to(std::f64::consts::PI); +/// Pi-expression, paired with whether `pi` actually appeared in the source: +/// `pi` -> `(PI, true)`, `*pi` -> `(num*PI, true)`, `` -> `(num, false)`. +/// The flag lets rotation/U3 tags enforce the half-turn `*pi` convention. +pub(crate) fn pi_expr_flagged<'src>() +-> impl Parser<'src, &'src str, (f64, bool), Extra<'src>> + Clone { + let pi_kw = just("pi").to((std::f64::consts::PI, true)); let num_then_pi = signed_float() .then(inline_pad().ignore_then(just("*pi")).or_not()) .map(|(n, suffix)| { if suffix.is_some() { - n * std::f64::consts::PI + (n * std::f64::consts::PI, true) } else { - n + (n, false) } }); choice((pi_kw, num_then_pi)) } +/// Pi-expression: `pi`, `*pi`, or plain number. Evaluates to f64. +pub(crate) fn pi_expr<'src>() -> impl Parser<'src, &'src str, f64, Extra<'src>> + Clone { + pi_expr_flagged().map(|(value, _)| value) +} + use crate::ast::shared::{Tag, TagParam}; /// `=` (Named) or `` (Positional). @@ -105,8 +113,8 @@ pub(crate) fn tag_param<'src>() -> impl Parser<'src, &'src str, TagParam, Extra< .then_ignore(inline_pad()) .then_ignore(just('=')) .then_ignore(inline_pad()) - .then(pi_expr()) - .map(|(key, value)| TagParam::Named { key, value }); + .then(pi_expr_flagged()) + .map(|(key, (value, had_pi))| TagParam::Named { key, value, had_pi }); let positional = pi_expr().map(TagParam::Positional); choice((named, positional)) } @@ -343,9 +351,10 @@ mod tests { assert_eq!(t.name, "R_X"); assert_eq!(t.params.len(), 1); match &t.params[0] { - TagParam::Named { key, value } => { + TagParam::Named { key, value, had_pi } => { assert_eq!(key, "theta"); assert!((value - 0.5 * std::f64::consts::PI).abs() < 1e-12); + assert!(had_pi); } other => panic!("{other:?}"), } diff --git a/crates/stim-parser/tests/extended.rs b/crates/stim-parser/tests/extended.rs index 1d866acf..19100f58 100644 --- a/crates/stim-parser/tests/extended.rs +++ b/crates/stim-parser/tests/extended.rs @@ -122,7 +122,7 @@ fn repeat_recurses_into_body() { #[test] fn repeat_promotes_extended_rotation_in_body() { - let p = parse_ok("REPEAT 2 { I[R_X(theta=0.25)] 0 }\n"); + let p = parse_ok("REPEAT 2 { I[R_X(theta=0.25*pi)] 0 }\n"); match &p.instructions[0] { ExtendedInstruction::Repeat { count, body, .. } => { assert_eq!(*count, 2); @@ -135,7 +135,7 @@ fn repeat_promotes_extended_rotation_in_body() { .. } => { assert!(matches!(axis, Axis::X)); - approx_eq(*theta, 0.25); + approx_eq(*theta, 0.25 * std::f64::consts::PI); assert_eq!(targets, &vec![0]); } other => panic!("{other:?}"), @@ -277,11 +277,11 @@ fn i_r_x_promotes_to_rotation_x() { #[test] fn i_r_y_promotes_to_rotation_y() { - let p = parse_ok("I[R_Y(theta=0.25)] 0\n"); + let p = parse_ok("I[R_Y(theta=0.25*pi)] 0\n"); match &p.instructions[0] { ExtendedInstruction::Rotation { axis, theta, .. } => { assert!(matches!(axis, Axis::Y)); - approx_eq(*theta, 0.25); + approx_eq(*theta, 0.25 * std::f64::consts::PI); } other => panic!("{other:?}"), } @@ -289,11 +289,11 @@ fn i_r_y_promotes_to_rotation_y() { #[test] fn i_r_z_promotes_to_rotation_z() { - let p = parse_ok("I[R_Z(theta=0.1)] 0\n"); + let p = parse_ok("I[R_Z(theta=0.1*pi)] 0\n"); match &p.instructions[0] { ExtendedInstruction::Rotation { axis, theta, .. } => { assert!(matches!(axis, Axis::Z)); - approx_eq(*theta, 0.1); + approx_eq(*theta, 0.1 * std::f64::consts::PI); } other => panic!("{other:?}"), } @@ -301,7 +301,7 @@ fn i_r_z_promotes_to_rotation_z() { #[test] fn i_u3_promotes_to_u3() { - let p = parse_ok("I[U3(theta=0.1, phi=0.2, lambda=0.3)] 0\n"); + let p = parse_ok("I[U3(theta=0.1*pi, phi=0.2*pi, lambda=0.3*pi)] 0\n"); match &p.instructions[0] { ExtendedInstruction::U3 { theta, @@ -310,9 +310,10 @@ fn i_u3_promotes_to_u3() { targets, span, } => { - approx_eq(*theta, 0.1); - approx_eq(*phi, 0.2); - approx_eq(*lambda, 0.3); + let pi = std::f64::consts::PI; + approx_eq(*theta, 0.1 * pi); + approx_eq(*phi, 0.2 * pi); + approx_eq(*lambda, 0.3 * pi); assert_eq!(targets, &vec![0]); assert_eq!(span.line(&p.line_map), 1); } diff --git a/crates/stim-parser/tests/proptest_ast.rs b/crates/stim-parser/tests/proptest_ast.rs index 392aa0d1..a7253800 100644 --- a/crates/stim-parser/tests/proptest_ast.rs +++ b/crates/stim-parser/tests/proptest_ast.rs @@ -398,15 +398,17 @@ fn ext_flat() -> impl Strategy { ) .prop_map(|(axis, theta, targets)| ExtendedInstruction::Rotation { axis, - theta, + // Rotation angles are stored in radians but always originate from + // a `*pi` tag, so only half-turn multiples are parser-producible. + theta: theta * std::f64::consts::PI, targets, span: span0(), }), (float_lit(), float_lit(), float_lit(), one_q_targets()).prop_map( |(theta, phi, lambda, targets)| ExtendedInstruction::U3 { - theta, - phi, - lambda, + theta: theta * std::f64::consts::PI, + phi: phi * std::f64::consts::PI, + lambda: lambda * std::f64::consts::PI, targets, span: span0(), } diff --git a/crates/stim-parser/tests/proptest_roundtrip.rs b/crates/stim-parser/tests/proptest_roundtrip.rs index 1f21cf56..44ac8733 100644 --- a/crates/stim-parser/tests/proptest_roundtrip.rs +++ b/crates/stim-parser/tests/proptest_roundtrip.rs @@ -40,10 +40,10 @@ fn instruction_fragment() -> impl Strategy { // Tagged sugar Just("S[T] 0\n".to_string()), Just("S_DAG[T] 1\n".to_string()), - Just("I[R_X(theta=0.5)] 0\n".to_string()), - Just("I[R_Y(theta=1.25)] 1\n".to_string()), - Just("I[R_Z(theta=-0.5)] 2\n".to_string()), - Just("I[U3(theta=0.5, phi=1.0, lambda=1.5)] 0\n".to_string()), + Just("I[R_X(theta=0.5*pi)] 0\n".to_string()), + Just("I[R_Y(theta=1.25*pi)] 1\n".to_string()), + Just("I[R_Z(theta=-0.5*pi)] 2\n".to_string()), + Just("I[U3(theta=0.5*pi, phi=1.0*pi, lambda=1.5*pi)] 0\n".to_string()), // Noise Just("DEPOLARIZE1(0.05) 0\n".to_string()), Just("DEPOLARIZE2(0.05) 0 1\n".to_string()), @@ -96,6 +96,16 @@ fn check_extended_fixpoint(src: &str) { assert_eq!(s1, s2, "extended printer is not a fixpoint"); } +/// A decimal with 0–4 fractional digits in `[-4, 4]`, rendered as a string. +/// Mirrors the angles a user actually writes; many are not binary-friendly, +/// so `theta/PI` would print a rounding tail without the printer's recovery. +fn decimal_coeff() -> impl Strategy { + (-40_000i32..=40_000, 0u32..=4).prop_map(|(n, scale)| { + let v = f64::from(n) / 10f64.powi(scale as i32); + format!("{v}") + }) +} + proptest! { #[test] fn raw_printer_is_fixpoint_on_fragments(src in program_source()) { @@ -106,4 +116,36 @@ proptest! { fn extended_printer_is_fixpoint_on_fragments(src in program_source()) { check_extended_fixpoint(&src); } + + /// Rotation/U3 angles are stored in radians as `c*PI` and re-emitted in + /// the `*pi` form. For any decimal coefficient a user might write, the + /// printer must (a) round-trip losslessly — `parse → print → parse` must + /// recover the exact stored radians — and (b) stay a byte-for-byte + /// fixpoint, never degrading into a `0.7599999999999999*pi` tail. + #[test] + fn rotation_pi_coeff_round_trips(c in decimal_coeff()) { + let src = format!("I[R_Z(theta={c}*pi)] 0\n"); + let ast1 = parse_extended(&src).expect("parse"); + let theta1 = rotation_theta(&ast1); + + let s1 = format!("{ast1}"); + prop_assert!( + !s1.contains("999999") && !s1.contains("000000"), + "printed a rounding tail for theta={c}*pi: {s1}" + ); + + let ast2 = parse_extended(&s1).expect("reparse"); + prop_assert_eq!(theta1.to_bits(), rotation_theta(&ast2).to_bits(), + "theta not recovered exactly for theta={}*pi", c); + prop_assert_eq!(format!("{ast2}"), s1, "printer not a fixpoint"); + } +} + +/// Extract the single rotation's `theta` from a one-instruction program. +fn rotation_theta(ast: &stim_parser::prelude::ExtendedProgram) -> f64 { + use stim_parser::prelude::ExtendedInstruction::Rotation; + match &ast.instructions[0] { + Rotation { theta, .. } => *theta, + other => panic!("expected a rotation, got {other:?}"), + } } diff --git a/crates/stim-parser/tests/roundtrip.rs b/crates/stim-parser/tests/roundtrip.rs index 36ca1560..81e1b98f 100644 --- a/crates/stim-parser/tests/roundtrip.rs +++ b/crates/stim-parser/tests/roundtrip.rs @@ -77,9 +77,9 @@ const EXTENDED_CORPUS: &[(&str, &str)] = &[ ("t_sugar", "S[T] 0 1\nS_DAG[T] 2\n"), ( "rotations", - "I[R_X(theta=0.5)] 0\nI[R_Y(theta=1.25)] 1\nI[R_Z(theta=-0.5)] 2\n", + "I[R_X(theta=0.5*pi)] 0\nI[R_Y(theta=1.25*pi)] 1\nI[R_Z(theta=-0.5*pi)] 2\n", ), - ("u3", "I[U3(theta=0.5, phi=1.0, lambda=1.5)] 0\n"), + ("u3", "I[U3(theta=0.5*pi, phi=1.0*pi, lambda=1.5*pi)] 0\n"), ("loss", "I_ERROR[loss](0.01) 0 1\n"), ( "correlated_loss", @@ -88,7 +88,7 @@ const EXTENDED_CORPUS: &[(&str, &str)] = &[ ("mpad_bits", "MPAD 0 1 0\nMPAD(0.01) 1 1 0 0\n"), ( "extended_in_repeat", - "REPEAT 3 {\n S[T] 0\n I[R_Z(theta=0.25)] 0\n M 0\n}\n", + "REPEAT 3 {\n S[T] 0\n I[R_Z(theta=0.25*pi)] 0\n M 0\n}\n", ), ( "mixed_extended_and_vanilla", @@ -160,12 +160,12 @@ REPEAT 2 { #[test] fn extended_printed_form_lowers_sugar_into_canonical_stim() { - let src = "S[T] 0\nI[R_X(theta=0.25)] 1\nI_ERROR[loss](0.01) 2\n"; + let src = "S[T] 0\nI[R_X(theta=0.25*pi)] 1\nI_ERROR[loss](0.01) 2\n"; let ast = parse_extended(src).unwrap(); let printed = format!("{ast}"); let expected = "\ S[T] 0 -I[R_X(theta=0.25)] 1 +I[R_X(theta=0.25*pi)] 1 I_ERROR[loss](0.01) 2 "; assert_eq!(printed, expected); diff --git a/crates/stim-parser/tests/tags.rs b/crates/stim-parser/tests/tags.rs index bee977b2..5898f0a1 100644 --- a/crates/stim-parser/tests/tags.rs +++ b/crates/stim-parser/tests/tags.rs @@ -30,9 +30,10 @@ fn parse_tag_named_param_pi_expr() { assert_eq!(tags.len(), 1); assert_eq!(tags[0].name, "R_X"); match &tags[0].params[..] { - [TagParam::Named { key, value }] => { + [TagParam::Named { key, value, had_pi }] => { assert_eq!(key, "theta"); approx_eq(*value, 0.5 * std::f64::consts::PI); + assert!(had_pi); } other => panic!("{other:?}"), } diff --git a/ppvm-python/test/test_stim_api.py b/ppvm-python/test/test_stim_api.py index 22122ce4..d537a435 100644 --- a/ppvm-python/test/test_stim_api.py +++ b/ppvm-python/test/test_stim_api.py @@ -180,7 +180,7 @@ def test_odd_two_qubit_sequence_raises_value_error(): [ "H 0\nCX 0 1\nM 0 1\n", "REPEAT 3 {\n X 0\n M 0\n}\n", - "S[T] 0\nI[R_X(theta=0.25)] 1\nI_ERROR[loss](0.01) 2\n", + "S[T] 0\nI[R_X(theta=0.25*pi)] 1\nI_ERROR[loss](0.01) 2\n", "MR 0\nCX rec[-1] 0\n", "MPP X0*Y1\nM 2\n", ],