diff --git a/crates/stim-parser/src/ast/extended.rs b/crates/stim-parser/src/ast/extended.rs index 6ff8b3b4..5b94cb04 100644 --- a/crates/stim-parser/src/ast/extended.rs +++ b/crates/stim-parser/src/ast/extended.rs @@ -6,7 +6,7 @@ use std::sync::Arc; -use crate::ast::shared::{AnnotationOp, Axis, GateOp, MeasureOp, MppOp, NoiseOp, Tag}; +use crate::ast::shared::{AnnotationOp, Axis, GateOp, MeasureOp, MppOp, NoiseOp}; use crate::diagnostics::{LineMap, Span}; #[derive(Debug, Clone, PartialEq)] @@ -51,12 +51,13 @@ pub enum ExtendedInstruction { span: Span, }, MPad { - tags: Vec, + tag: String, prob: Option, bits: Vec, span: Span, }, Repeat { + tag: String, count: u64, body: Vec, span: Span, @@ -167,13 +168,14 @@ mod tests { fn measurement_count_scales_with_repeat() { let m = ExtendedInstruction::Measure(MeasureOp { name: MeasureName::M, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![0, 1], span: span(), }); let prog = ExtendedProgram { instructions: vec![ExtendedInstruction::Repeat { + tag: String::new(), count: 3, body: vec![m], span: span(), @@ -190,14 +192,14 @@ mod tests { instructions: vec![ ExtendedInstruction::Gate(GateOp { name: GateName::H, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![Target::Qubit(0), Target::Qubit(4)], span: span(), }), ExtendedInstruction::Measure(MeasureOp { name: MeasureName::M, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![2], span: span(), @@ -212,10 +214,11 @@ mod tests { fn num_qubits_recurses_into_repeat() { let prog = ExtendedProgram { instructions: vec![ExtendedInstruction::Repeat { + tag: String::new(), count: 3, body: vec![ExtendedInstruction::Measure(MeasureOp { name: MeasureName::M, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![7], span: span(), @@ -241,7 +244,7 @@ mod tests { fn gate_op_is_shared_with_vanilla() { let _ = ExtendedInstruction::Gate(GateOp { name: GateName::H, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![], span: span(), diff --git a/crates/stim-parser/src/ast/shared.rs b/crates/stim-parser/src/ast/shared.rs index d1c8a803..4104f941 100644 --- a/crates/stim-parser/src/ast/shared.rs +++ b/crates/stim-parser/src/ast/shared.rs @@ -65,25 +65,6 @@ pub struct PauliFactor { pub qubit: usize, } -#[derive(Debug, Clone, PartialEq)] -pub struct Tag { - pub name: String, - pub params: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum TagParam { - Positional(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. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Axis { @@ -99,7 +80,7 @@ pub enum Axis { #[derive(Debug, Clone, PartialEq)] pub struct GateOp { pub name: GateName, - pub tags: Vec, + pub tag: String, pub args: Vec, pub targets: Vec, pub span: Span, @@ -108,7 +89,7 @@ pub struct GateOp { #[derive(Debug, Clone, PartialEq)] pub struct NoiseOp { pub name: NoiseName, - pub tags: Vec, + pub tag: String, pub args: Vec, pub targets: Vec, pub span: Span, @@ -117,7 +98,7 @@ pub struct NoiseOp { #[derive(Debug, Clone, PartialEq)] pub struct MeasureOp { pub name: MeasureName, - pub tags: Vec, + pub tag: String, pub args: Vec, pub targets: Vec, pub span: Span, @@ -126,6 +107,7 @@ pub struct MeasureOp { #[derive(Debug, Clone, PartialEq)] pub struct AnnotationOp { pub kind: AnnotationKind, + pub tag: String, pub args: Vec, pub targets: Vec, pub span: Span, @@ -133,7 +115,7 @@ pub struct AnnotationOp { #[derive(Debug, Clone, PartialEq)] pub struct MppOp { - pub tags: Vec, + pub tag: String, pub args: Vec, pub products: Vec>, pub span: Span, diff --git a/crates/stim-parser/src/ast/vanilla.rs b/crates/stim-parser/src/ast/vanilla.rs index 3c0cffec..65df4e3c 100644 --- a/crates/stim-parser/src/ast/vanilla.rs +++ b/crates/stim-parser/src/ast/vanilla.rs @@ -1,12 +1,12 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! Vanilla Stim AST. Tags are preserved verbatim; the parser does not -//! resolve the Stim dialect — that is the consumer's responsibility. +//! Vanilla Stim AST. Tags are preserved as decoded opaque strings; the parser +//! does not resolve the Stim dialect. use std::sync::Arc; -use crate::ast::shared::{AnnotationOp, GateOp, MeasureOp, MppOp, NoiseOp, Tag}; +use crate::ast::shared::{AnnotationOp, GateOp, MeasureOp, MppOp, NoiseOp}; use crate::diagnostics::{LineMap, Span}; #[derive(Debug, Clone, PartialEq)] @@ -17,12 +17,13 @@ pub enum Instruction { Annotation(AnnotationOp), Mpp(MppOp), MPad { - tags: Vec, + tag: String, prob: Option, bits: Vec, span: Span, }, Repeat { + tag: String, count: u64, body: Vec, span: Span, @@ -57,7 +58,7 @@ mod tests { let p = Program { instructions: vec![Instruction::Gate(GateOp { name: GateName::H, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![], span: Span::new(0, 1), @@ -72,7 +73,7 @@ mod tests { let g = || { Instruction::Gate(GateOp { name: GateName::H, - tags: vec![], + tag: String::new(), args: vec![], targets: vec![], span: Span::new(0, 1), diff --git a/crates/stim-parser/src/lib.rs b/crates/stim-parser/src/lib.rs index 4d144729..b74462c5 100644 --- a/crates/stim-parser/src/lib.rs +++ b/crates/stim-parser/src/lib.rs @@ -54,7 +54,7 @@ pub fn parse_extended(src: &str) -> Result { pub mod prelude { pub use crate::ast::{ AnnotationOp, Axis, ExtendedInstruction, ExtendedProgram, GateOp, Instruction, MeasureOp, - MppOp, NoiseOp, PauliAxis, PauliFactor, Program, Tag, TagParam, Target, + MppOp, NoiseOp, PauliAxis, PauliFactor, Program, Target, }; pub use crate::diagnostics::{ Diagnostic, DiagnosticSink, Diagnostics, Flow, LineMap, Severity, Span, diff --git a/crates/stim-parser/src/pipeline/lower.rs b/crates/stim-parser/src/pipeline/lower.rs index caed9447..04233424 100644 --- a/crates/stim-parser/src/pipeline/lower.rs +++ b/crates/stim-parser/src/pipeline/lower.rs @@ -10,14 +10,69 @@ use std::sync::Arc; +use chumsky::error::Rich; +use chumsky::extra; +use chumsky::prelude::*; + use crate::ast::extended::{ExtendedInstruction, ExtendedProgram}; -use crate::ast::shared::{Axis, GateOp, NoiseOp, Tag, TagParam, Target}; +use crate::ast::shared::{Axis, GateOp, NoiseOp, Target}; use crate::ast::vanilla::{Instruction, Program}; use crate::diagnostics::{Aborted, DiagnosticSink, Span}; use crate::instructions::{GateName, NoiseName}; use super::emit_skip; +type TagExtra<'src> = extra::Err>; + +#[derive(Debug, Clone, PartialEq)] +struct StructuredTag { + name: String, + params: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +enum StructuredTagParam { + Positional(f64), + Named { + key: String, + value: f64, + had_pi: bool, + }, +} + +fn structured_tag<'src>() -> impl Parser<'src, &'src str, StructuredTag, TagExtra<'src>> + Clone { + use crate::syntax::{ident, inline_pad, pi_expr, pi_expr_flagged}; + + let named = ident() + .then_ignore(inline_pad()) + .then_ignore(just('=')) + .then_ignore(inline_pad()) + .then(pi_expr_flagged()) + .map(|(key, (value, had_pi))| StructuredTagParam::Named { key, value, had_pi }); + let positional = pi_expr().map(StructuredTagParam::Positional); + let params = choice((named, positional)) + .separated_by(inline_pad().then(just(',')).then(inline_pad())) + .allow_trailing() + .collect::>() + .delimited_by(just('(').then(inline_pad()), inline_pad().then(just(')'))); + + inline_pad() + .ignore_then(ident().then(params.or_not())) + .then_ignore(inline_pad()) + .map(|(name, params)| StructuredTag { + name, + params: params.unwrap_or_default(), + }) +} + +fn parse_structured_tag(src: &str) -> Option { + structured_tag() + .then_ignore(end()) + .parse(src) + .into_result() + .ok() +} + /// Lower a vanilla [`Program`] into an [`ExtendedProgram`], forwarding every /// recoverable error to the sink. pub(crate) fn lower( @@ -65,7 +120,7 @@ fn lower_one( Instruction::Annotation(op) => Ok(Some(ExtendedInstruction::Annotation(op))), Instruction::Mpp(op) => Ok(Some(ExtendedInstruction::Mpp(op))), Instruction::MPad { - tags, + tag, prob, bits, span, @@ -74,15 +129,25 @@ fn lower_one( return Ok(None); }; Ok(Some(ExtendedInstruction::MPad { - tags, + tag, prob, bits, span, })) } - Instruction::Repeat { count, body, span } => { + Instruction::Repeat { + tag, + count, + body, + span, + } => { let body = lower_slice(body, sink)?; - Ok(Some(ExtendedInstruction::Repeat { count, body, span })) + Ok(Some(ExtendedInstruction::Repeat { + tag, + count, + body, + span, + })) } } } @@ -95,24 +160,15 @@ fn lower_gate( let GateOp { name, - tags, + tag, args, targets, span, } = op; match name { - // Native T / T_DAG mnemonics lower to the same sugar as `S[T]` / `S_DAG[T]`. + // Tags on native T/T_DAG do not affect simulation. 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); }; @@ -122,89 +178,58 @@ fn lower_gate( ExtendedInstruction::TDag { targets, span } })) } - S | SDag => match tags.as_slice() { - [] => Ok(Some(ExtendedInstruction::Gate(GateOp { - name, - tags, - args, - targets, - span, - }))), - [t] if t.name == "T" => { - if require_no_params(t, name.canonical_name(), span, sink)?.is_none() { - return Ok(None); - } + S | SDag => { + if tag == "T" { let Some(targets) = qubit_targets(targets, name.canonical_name(), span, sink)? else { return Ok(None); }; - Ok(Some(if matches!(name, S) { + return Ok(Some(if matches!(name, S) { ExtendedInstruction::T { targets, span } } else { ExtendedInstruction::TDag { targets, span } - })) + })); } - [t] => invalid_tag(&t.name, name.canonical_name(), span, "expected [T]", sink), - _ => invalid_tag( - &tags[0].name, - name.canonical_name(), - span, - "expected exactly one tag", - sink, - ), - }, - Identity => match tags.as_slice() { - [] => Ok(Some(ExtendedInstruction::Gate(GateOp { + Ok(Some(ExtendedInstruction::Gate(GateOp { name, - tags, + tag, args, targets, span, - }))), - [t] => { - let Some(targets) = qubit_targets(targets, "I", span, sink)? else { - return Ok(None); - }; - interpret_identity_tag(t, targets, span, sink) - } - _ => invalid_tag(&tags[0].name, "I", span, "expected exactly one tag", sink), - }, - _ => Ok(Some(ExtendedInstruction::Gate(GateOp { + }))) + } + Identity if tag.is_empty() => Ok(Some(ExtendedInstruction::Gate(GateOp { name, - tags, + tag, args, targets, span, }))), - } -} - -/// `Ok(Some(()))` — the tag carried no parameters. `Ok(None)` — a violation was -/// emitted and the sink chose to continue. `Err(Aborted)` — abort the stage. -fn require_no_params( - tag: &Tag, - instruction: &str, - span: Span, - sink: &mut dyn DiagnosticSink, -) -> Result, Aborted> { - if !tag.params.is_empty() { - return invalid_tag::<()>( - &tag.name, - instruction, + Identity => { + let Some(targets) = qubit_targets(targets, "I", span, sink)? else { + return Ok(None); + }; + interpret_identity_tag(&tag, targets, span, sink) + } + _ => Ok(Some(ExtendedInstruction::Gate(GateOp { + name, + tag, + args, + targets, span, - "tag must have no parameters", - sink, - ); + }))), } - Ok(Some(())) } fn interpret_identity_tag( - tag: &Tag, + tag_text: &str, targets: Vec, span: Span, sink: &mut dyn DiagnosticSink, ) -> Result, Aborted> { + let Some(tag) = parse_structured_tag(tag_text) else { + return invalid_tag(tag_text, "I", span, "expected a PPVM modifier", sink); + }; let axis = match tag.name.as_str() { "R_X" => Some(Axis::X), "R_Y" => Some(Axis::Y), @@ -213,7 +238,7 @@ fn interpret_identity_tag( }; if let Some(axis) = axis { - let Some([theta]) = exact_named_params(tag, ["theta"], "I", span, sink)? else { + let Some([theta]) = exact_named_params(&tag, ["theta"], "I", span, sink)? else { return Ok(None); }; return Ok(Some(ExtendedInstruction::Rotation { @@ -226,7 +251,7 @@ fn interpret_identity_tag( if tag.name == "U3" { let Some([theta, phi, lambda]) = - exact_named_params(tag, ["theta", "phi", "lambda"], "I", span, sink)? + exact_named_params(&tag, ["theta", "phi", "lambda"], "I", span, sink)? else { return Ok(None); }; @@ -256,17 +281,14 @@ fn lower_noise( let NoiseOp { name, - tags, + tag, args, targets, span, } = op; - match (name, tags.as_slice()) { - (IError, [t]) if t.name == "loss" => { - if require_no_params(t, name.canonical_name(), span, sink)?.is_none() { - return Ok(None); - } + match (name, tag.as_str()) { + (IError, "loss") => { if args.len() != 1 { return invalid_tag( "loss", @@ -282,10 +304,7 @@ fn lower_noise( span, })) } - (IError, [t]) if t.name == "correlated_loss" => { - if require_no_params(t, name.canonical_name(), span, sink)?.is_none() { - return Ok(None); - } + (IError, "correlated_loss") => { if targets.is_empty() || !targets.len().is_multiple_of(2) { return invalid_tag( "correlated_loss", @@ -317,30 +336,23 @@ fn lower_noise( span, })) } - (IError, []) => invalid_tag( + (IError, "") => invalid_tag( "", name.canonical_name(), span, "I_ERROR requires a [loss] or [correlated_loss] tag", sink, ), - (IError, [t]) => invalid_tag( - &t.name, + (IError, other) => invalid_tag( + other, name.canonical_name(), span, "expected [loss] or [correlated_loss]", sink, ), - (IError, _) => invalid_tag( - &tags[0].name, - name.canonical_name(), - span, - "expected exactly one tag", - sink, - ), _ => Ok(Some(ExtendedInstruction::Noise(NoiseOp { name, - tags, + tag, args, targets, span, @@ -362,7 +374,7 @@ fn invalid_tag( sink, span, "invalid-tag", - format!("invalid tag '{tag_name}' on {instruction}: {message}"), + format!("invalid tag {tag_name:?} on {instruction}: {message}"), ) } @@ -434,7 +446,7 @@ fn pair_targets(targets: &[usize]) -> Vec<(usize, usize)> { /// positional params, no unexpected/duplicate/missing keys — and return their /// values in `required` order. Any violation is reported as `invalid-tag`. fn exact_named_params( - tag: &Tag, + tag: &StructuredTag, required: [&str; N], instruction: &str, span: Span, @@ -445,7 +457,7 @@ fn exact_named_params( for param in &tag.params { match param { - TagParam::Positional(_) => { + StructuredTagParam::Positional(_) => { return invalid_tag( &tag.name, instruction, @@ -454,7 +466,7 @@ fn exact_named_params( sink, ); } - TagParam::Named { key, value, had_pi } => { + StructuredTagParam::Named { key, value, had_pi } => { let Some(index) = required.iter().position(|required_key| key == required_key) else { return invalid_tag( @@ -611,17 +623,21 @@ mod tests { } #[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")); + fn bare_t_ignores_tag() { + let prog = lower_extended("T[foo] 0").expect("lower"); + assert!(matches!( + &prog.instructions[0], + ExtendedInstruction::T { targets, .. } if targets == &vec![0] + )); } #[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")); + fn bare_t_dag_ignores_tag() { + let prog = lower_extended("T_DAG[foo] 0").expect("lower"); + assert!(matches!( + &prog.instructions[0], + ExtendedInstruction::TDag { targets, .. } if targets == &vec![0] + )); } #[test] @@ -763,14 +779,20 @@ mod tests { } #[test] - fn s_with_unknown_tag_is_invalid_tag() { - let err = lower_extended("S[BOGUS] 0").unwrap_err(); - assert_eq!(err.last().unwrap().code, Some("invalid-tag")); + fn s_with_unknown_tag_passes_through() { + let prog = lower_extended("S[BOGUS] 0").expect("lower"); + assert!(matches!( + &prog.instructions[0], + ExtendedInstruction::Gate(op) if op.name == GateName::S && op.tag == "BOGUS" + )); } #[test] - fn s_t_tag_with_params_is_invalid_tag() { - let err = lower_extended("S[T(theta=0.5)] 0").unwrap_err(); - assert_eq!(err.last().unwrap().code, Some("invalid-tag")); + fn s_non_exact_t_tag_passes_through() { + let prog = lower_extended("S[T(theta=0.5)] 0").expect("lower"); + assert!(matches!( + &prog.instructions[0], + ExtendedInstruction::Gate(op) if op.name == GateName::S && op.tag == "T(theta=0.5)" + )); } } diff --git a/crates/stim-parser/src/pipeline/validate.rs b/crates/stim-parser/src/pipeline/validate.rs index 343ed29e..d051db46 100644 --- a/crates/stim-parser/src/pipeline/validate.rs +++ b/crates/stim-parser/src/pipeline/validate.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use crate::ast::shared::{ - AnnotationOp, GateOp, MeasureOp, MppOp, NoiseOp, PauliAxis, PauliFactor, Tag, Target, + AnnotationOp, GateOp, MeasureOp, MppOp, NoiseOp, PauliAxis, PauliFactor, Target, }; use crate::ast::vanilla::{Instruction, Program}; use crate::diagnostics::{Aborted, DiagnosticSink, LineMap, Span}; @@ -62,7 +62,7 @@ fn validate_node( match node { RawSyntaxNode::Instruction { name, - tags, + tag, args, targets, span, @@ -103,7 +103,7 @@ fn validate_node( ); } return Ok(Some(Instruction::Mpp(MppOp { - tags, + tag, args, products, span, @@ -168,16 +168,26 @@ fn validate_node( Ok(Some(build_instruction( entry, - tags, + tag, args, parsed_targets, span, ))) } - RawSyntaxNode::Repeat { count, body, span } => { + RawSyntaxNode::Repeat { + tag, + count, + body, + span, + } => { let span: Span = span.into(); let body = validate_slice(body, line_map, sink)?; - Ok(Some(Instruction::Repeat { count, body, span })) + Ok(Some(Instruction::Repeat { + tag, + count, + body, + span, + })) } } } @@ -272,7 +282,7 @@ fn qubit_indices(targets: Vec) -> Vec { fn build_instruction( entry: TableEntry, - tags: Vec, + tag: String, args: Vec, targets: Vec, span: Span, @@ -280,33 +290,34 @@ fn build_instruction( match entry.kind { EntryKind::Gate(name) => Instruction::Gate(GateOp { name, - tags, + tag, args, targets, span, }), EntryKind::Noise(name) => Instruction::Noise(NoiseOp { name, - tags, + tag, args, targets: qubit_indices(targets), span, }), EntryKind::Measure(name) => Instruction::Measure(MeasureOp { name, - tags, + tag, args, targets: qubit_indices(targets), span, }), EntryKind::Annotation(kind) => Instruction::Annotation(AnnotationOp { kind, + tag, args, targets: qubit_indices(targets), span, }), EntryKind::MPad => Instruction::MPad { - tags, + tag, prob: args.into_iter().next(), bits: qubit_indices(targets), span, @@ -317,7 +328,7 @@ fn build_instruction( #[cfg(test)] mod tests { use super::*; - use crate::ast::shared::{Tag, TagParam, Target}; + use crate::ast::shared::Target; use crate::ast::vanilla::Instruction; use crate::diagnostics::{Collect, Diagnostic, FailFast, LineMap}; use crate::instructions::{GateName, MeasureName, NoiseName}; @@ -337,7 +348,7 @@ mod tests { ) -> RawSyntaxNode { RawSyntaxNode::Instruction { name: name.to_string(), - tags: vec![], + tag: String::new(), args, targets: targets .into_iter() @@ -358,7 +369,7 @@ mod tests { ) -> RawSyntaxNode { RawSyntaxNode::Instruction { name: name.to_string(), - tags: vec![], + tag: String::new(), args, targets: targets .into_iter() @@ -371,10 +382,10 @@ mod tests { } } - fn instr_with_tags(name: &str, tags: Vec) -> RawSyntaxNode { + fn instr_with_tag(name: &str, tag: &str) -> RawSyntaxNode { RawSyntaxNode::Instruction { name: name.to_string(), - tags, + tag: tag.to_string(), args: vec![], targets: vec![RawTarget { text: "0".to_string(), @@ -514,6 +525,7 @@ mod tests { #[test] fn repeat_body_is_validated_recursively() { let nodes = vec![RawSyntaxNode::Repeat { + tag: String::new(), count: 3, body: vec![instr("H", vec![], vec!["0"], (11, 12))], span: SimpleSpan::from(0..6), @@ -521,7 +533,9 @@ mod tests { let line_map = Arc::new(LineMap::new("REPEAT 3 { H 0 }")); let prog = ok_program(nodes, &line_map); match &prog.instructions[0] { - Instruction::Repeat { count, body, span } => { + Instruction::Repeat { + count, body, span, .. + } => { assert_eq!(*count, 3); assert_eq!(span.line(&prog.line_map), 1); assert!(matches!( @@ -538,31 +552,11 @@ mod tests { } #[test] - fn tags_pass_through_validator() { - let nodes = vec![instr_with_tags( - "H", - vec![Tag { - name: "R".to_string(), - params: vec![ - TagParam::Positional(0.5), - TagParam::Named { - key: "theta".to_string(), - value: 0.25, - had_pi: false, - }, - ], - }], - )]; + fn tag_passes_through_validator() { + let nodes = vec![instr_with_tag("H", "R(0.5, theta=0.25)")]; let prog = ok_program(nodes, &lm()); match &prog.instructions[0] { - Instruction::Gate(GateOp { tags, .. }) => { - assert_eq!(tags[0].name, "R"); - 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 - )); - } + Instruction::Gate(GateOp { tag, .. }) => assert_eq!(tag, "R(0.5, theta=0.25)"), other => panic!("{other:?}"), } } diff --git a/crates/stim-parser/src/print/mod.rs b/crates/stim-parser/src/print/mod.rs index 411553d6..c7a34de4 100644 --- a/crates/stim-parser/src/print/mod.rs +++ b/crates/stim-parser/src/print/mod.rs @@ -2,14 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 //! First-class canonical printer for both AST layers. Output is canonical -//! Stim — 4-space REPEAT indentation, `[tags](args) targets` ordering — and +//! Stim — 4-space REPEAT indentation, `[tag](args) targets` ordering — and //! round-trips: parse → print → parse is a fixpoint. use std::borrow::Cow; use std::fmt; use crate::ast::shared::{ - AnnotationOp, Axis, GateOp, MeasureOp, MppOp, NoiseOp, PauliFactor, Tag, TagParam, Target, + AnnotationOp, Axis, GateOp, MeasureOp, MppOp, NoiseOp, PauliFactor, Target, }; use crate::ast::{ExtendedInstruction, ExtendedProgram, Instruction, Program}; @@ -51,34 +51,18 @@ fn write_indent(out: &mut dyn fmt::Write, opts: &PrintOptions, depth: usize) -> Ok(()) } -fn write_tags(out: &mut dyn fmt::Write, tags: &[Tag]) -> fmt::Result { - if tags.is_empty() { +fn write_tag(out: &mut dyn fmt::Write, tag: &str) -> fmt::Result { + if tag.is_empty() { return Ok(()); } out.write_str("[")?; - for (i, tag) in tags.iter().enumerate() { - if i > 0 { - out.write_str(", ")?; - } - out.write_str(&tag.name)?; - if !tag.params.is_empty() { - out.write_str("(")?; - for (j, p) in tag.params.iter().enumerate() { - if j > 0 { - out.write_str(", ")?; - } - match p { - TagParam::Positional(v) => write!(out, "{}", FloatLit(*v))?, - TagParam::Named { key, value, had_pi } => { - if *had_pi { - write!(out, "{key}={}*pi", FloatLit(pi_coeff(*value)))?; - } else { - write!(out, "{key}={}", FloatLit(*value))?; - } - } - } - } - out.write_str(")")?; + for c in tag.chars() { + match c { + '\n' => out.write_str("\\n")?, + '\r' => out.write_str("\\r")?, + '\\' => out.write_str("\\B")?, + ']' => out.write_str("\\C")?, + _ => out.write_char(c)?, } } out.write_str("]") @@ -124,10 +108,13 @@ fn write_repeat_block( out: &mut dyn fmt::Write, opts: &PrintOptions, depth: usize, + tag: &str, count: u64, body: &[T], ) -> fmt::Result { - writeln!(out, "REPEAT {count} {{")?; + out.write_str("REPEAT")?; + write_tag(out, tag)?; + writeln!(out, " {count} {{")?; for instr in body { instr.print(out, opts, depth + 1)?; } @@ -207,7 +194,7 @@ fn pi_coeff(value: f64) -> f64 { impl StimPrint for GateOp { fn print(&self, out: &mut dyn fmt::Write, _opts: &PrintOptions, _depth: usize) -> fmt::Result { out.write_str(self.name.canonical_name())?; - write_tags(out, &self.tags)?; + write_tag(out, &self.tag)?; write_args(out, &self.args)?; write_targets(out, &self.targets) } @@ -216,7 +203,7 @@ impl StimPrint for GateOp { impl StimPrint for NoiseOp { fn print(&self, out: &mut dyn fmt::Write, _opts: &PrintOptions, _depth: usize) -> fmt::Result { out.write_str(self.name.canonical_name())?; - write_tags(out, &self.tags)?; + write_tag(out, &self.tag)?; write_args(out, &self.args)?; write_usize_targets(out, &self.targets) } @@ -225,7 +212,7 @@ impl StimPrint for NoiseOp { impl StimPrint for MeasureOp { fn print(&self, out: &mut dyn fmt::Write, _opts: &PrintOptions, _depth: usize) -> fmt::Result { out.write_str(self.name.canonical_name())?; - write_tags(out, &self.tags)?; + write_tag(out, &self.tag)?; write_args(out, &self.args)?; write_usize_targets(out, &self.targets) } @@ -234,6 +221,7 @@ impl StimPrint for MeasureOp { impl StimPrint for AnnotationOp { fn print(&self, out: &mut dyn fmt::Write, _opts: &PrintOptions, _depth: usize) -> fmt::Result { out.write_str(self.kind.canonical_name())?; + write_tag(out, &self.tag)?; write_args(out, &self.args)?; write_usize_targets(out, &self.targets) } @@ -242,7 +230,7 @@ impl StimPrint for AnnotationOp { impl StimPrint for MppOp { fn print(&self, out: &mut dyn fmt::Write, _opts: &PrintOptions, _depth: usize) -> fmt::Result { out.write_str(crate::instructions::MeasureName::MPP.canonical_name())?; - write_tags(out, &self.tags)?; + write_tag(out, &self.tag)?; write_args(out, &self.args)?; write_mpp_products(out, &self.products) } @@ -262,17 +250,19 @@ impl StimPrint for Instruction { Instruction::Annotation(op) => op.print(out, opts, depth)?, Instruction::Mpp(op) => op.print(out, opts, depth)?, Instruction::MPad { - tags, prob, bits, .. + tag, prob, bits, .. } => { out.write_str("MPAD")?; - write_tags(out, tags)?; + write_tag(out, tag)?; if let Some(p) = prob { write!(out, "({})", FloatLit(*p))?; } write_usize_targets(out, bits)?; } - Instruction::Repeat { count, body, .. } => { - write_repeat_block(out, opts, depth, *count, body)?; + Instruction::Repeat { + tag, count, body, .. + } => { + write_repeat_block(out, opts, depth, tag, *count, body)?; } } writeln!(out) @@ -369,10 +359,10 @@ impl StimPrint for ExtendedInstruction { } } ExtendedInstruction::MPad { - tags, prob, bits, .. + tag, prob, bits, .. } => { out.write_str("MPAD")?; - write_tags(out, tags)?; + write_tag(out, tag)?; if let Some(p) = prob { write!(out, "({})", FloatLit(*p))?; } @@ -380,8 +370,10 @@ impl StimPrint for ExtendedInstruction { write!(out, " {}", u8::from(bit))?; } } - ExtendedInstruction::Repeat { count, body, .. } => { - write_repeat_block(out, opts, depth, *count, body)?; + ExtendedInstruction::Repeat { + tag, count, body, .. + } => { + write_repeat_block(out, opts, depth, tag, *count, body)?; } } writeln!(out) diff --git a/crates/stim-parser/src/syntax/grammar.rs b/crates/stim-parser/src/syntax/grammar.rs index 446cd52c..b09643d8 100644 --- a/crates/stim-parser/src/syntax/grammar.rs +++ b/crates/stim-parser/src/syntax/grammar.rs @@ -4,7 +4,7 @@ //! Chumsky 0.12 grammar for Stim source. //! //! Reads top-to-bottom: whitespace/comments -> numbers -> pi-expressions -> -//! identifiers -> tags -> args -> targets -> instruction line -> REPEAT block -> +//! identifiers -> tag -> args -> targets -> instruction line -> REPEAT block -> //! program. Pure syntax; no table lookups. use chumsky::error::Rich; @@ -107,40 +107,19 @@ pub(crate) fn pi_expr<'src>() -> impl Parser<'src, &'src str, f64, Extra<'src>> pi_expr_flagged().map(|(value, _)| value) } -use crate::ast::shared::{Tag, TagParam}; - -/// `=` (Named) or `` (Positional). -pub(crate) fn tag_param<'src>() -> impl Parser<'src, &'src str, TagParam, Extra<'src>> + Clone { - let named = ident() - .then_ignore(inline_pad()) - .then_ignore(just('=')) - .then_ignore(inline_pad()) - .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)) -} - -/// Tag: `` or `(, ...)`. -pub(crate) fn tag<'src>() -> impl Parser<'src, &'src str, Tag, Extra<'src>> + Clone { - let params = tag_param() - .separated_by(inline_pad().then(just(',')).then(inline_pad())) - .allow_trailing() - .collect::>() - .delimited_by(just('(').then(inline_pad()), inline_pad().then(just(')'))); - ident().then(params.or_not()).map(|(name, params)| Tag { - name, - params: params.unwrap_or_default(), - }) -} - -/// `[tag, tag, ...]`. -pub(crate) fn tags_block<'src>() -> impl Parser<'src, &'src str, Vec, Extra<'src>> + Clone { - tag() - .separated_by(inline_pad().then(just(',')).then(inline_pad())) - .allow_trailing() - .collect::>() - .delimited_by(just('[').then(inline_pad()), inline_pad().then(just(']'))) +/// One decoded Stim tag. The contents are opaque to the syntax layer. +pub(crate) fn tag<'src>() -> impl Parser<'src, &'src str, String, Extra<'src>> + Clone { + let escaped = just('\\').ignore_then(choice(( + just('n').to('\n'), + just('r').to('\r'), + just('B').to('\\'), + just('C').to(']'), + ))); + let literal = any().filter(|c: &char| !matches!(*c, '\\' | ']' | '\r' | '\n')); + choice((escaped, literal)) + .repeated() + .collect::() + .delimited_by(just('['), just(']')) } /// `(pi_expr, pi_expr, ...)`. @@ -170,19 +149,19 @@ pub(crate) fn target_lexeme<'src>() -> impl Parser<'src, &'src str, RawTarget, E }) } -/// ` []? ()?`. Returns name, tags, args, and the +/// ` []? ()?`. Returns name, tag, args, and the /// span of the identifier (used for line-number reporting). pub(crate) fn instruction_head<'src>() --> impl Parser<'src, &'src str, (String, Vec, Vec, SimpleSpan), Extra<'src>> + Clone +-> impl Parser<'src, &'src str, (String, String, Vec, SimpleSpan), Extra<'src>> + Clone { ident() .map_with(|name, e| (name, e.span())) - .then(tags_block().or_not()) + .then(tag().or_not()) .then(args_block().or_not()) - .map(|(((name, span), tags), args)| { + .map(|(((name, span), tag), args)| { ( name, - tags.unwrap_or_default(), + tag.unwrap_or_default(), args.unwrap_or_default(), span, ) @@ -214,9 +193,9 @@ pub(crate) fn instruction_line<'src>() .collect::>(), ) .map( - |((name, tags, args, span), targets)| RawSyntaxNode::Instruction { + |((name, tag, args, span), targets)| RawSyntaxNode::Instruction { name, - tags, + tag, args, targets, span, @@ -241,6 +220,7 @@ fn repeat_block<'src>( }); just("REPEAT") .map_with(|_, e| e.span()) + .then(tag().or_not()) .then_ignore(inline_ws1()) .then(digits) .then_ignore(inline_pad()) @@ -249,7 +229,12 @@ fn repeat_block<'src>( .then(body) .then_ignore(pad()) .then_ignore(just('}')) - .map(|((span, count), body)| RawSyntaxNode::Repeat { count, body, span }) + .map(|(((span, tag), count), body)| RawSyntaxNode::Repeat { + tag: tag.unwrap_or_default(), + count, + body, + span, + }) } /// Top-level program parser. Recursively defines the body shared by @@ -269,7 +254,6 @@ pub(crate) fn program_parser<'src>() -> impl Parser<'src, &'src str, Vec { - assert_eq!(key, "theta"); - assert!((value - 0.5 * std::f64::consts::PI).abs() < 1e-12); - assert!(had_pi); - } - other => panic!("{other:?}"), - } - } - - #[test] - fn tags_block_parses_multiple_tags() { - let ts = run(tags_block(), "[T, R(0.5)]"); - assert_eq!(ts.len(), 2); - assert_eq!(ts[0].name, "T"); - assert_eq!(ts[1].name, "R"); + fn tag_is_one_opaque_decoded_string() { + assert_eq!(run(tag(), "[T, R(0.5)]"), "T, R(0.5)"); + assert_eq!(run(tag(), "[a\\n\\r\\B\\Cz]"), "a\n\r\\]z"); } #[test] @@ -404,18 +356,17 @@ mod tests { #[test] fn instruction_head_with_tags_and_args() { - let (name, tags, args, _span) = run(instruction_head(), "S[T](0.5)"); + let (name, tag, args, _span) = run(instruction_head(), "S[T](0.5)"); assert_eq!(name, "S"); - assert_eq!(tags.len(), 1); - assert_eq!(tags[0].name, "T"); + assert_eq!(tag, "T"); assert_eq!(args, vec![0.5]); } #[test] fn instruction_head_no_tags_no_args() { - let (name, tags, args, _span) = run(instruction_head(), "H"); + let (name, tag, args, _span) = run(instruction_head(), "H"); assert_eq!(name, "H"); - assert!(tags.is_empty()); + assert!(tag.is_empty()); assert!(args.is_empty()); } diff --git a/crates/stim-parser/src/syntax/mod.rs b/crates/stim-parser/src/syntax/mod.rs index 48baeae4..31ce0043 100644 --- a/crates/stim-parser/src/syntax/mod.rs +++ b/crates/stim-parser/src/syntax/mod.rs @@ -7,7 +7,7 @@ mod grammar; pub(crate) mod raw; -pub(crate) use grammar::program_parser; +pub(crate) use grammar::{ident, inline_pad, pi_expr, pi_expr_flagged, program_parser}; pub(crate) use raw::RawSyntaxTree; pub(crate) use raw::{RawSyntaxNode, RawTarget}; diff --git a/crates/stim-parser/src/syntax/raw.rs b/crates/stim-parser/src/syntax/raw.rs index 6ff6454a..9458fe84 100644 --- a/crates/stim-parser/src/syntax/raw.rs +++ b/crates/stim-parser/src/syntax/raw.rs @@ -7,20 +7,19 @@ use chumsky::span::SimpleSpan; -use crate::ast::shared::Tag; - pub(crate) type RawSyntaxTree = Vec; #[derive(Debug, Clone)] pub(crate) enum RawSyntaxNode { Instruction { name: String, - tags: Vec, + tag: String, args: Vec, targets: Vec, span: SimpleSpan, }, Repeat { + tag: String, count: u64, body: Vec, span: SimpleSpan, diff --git a/crates/stim-parser/tests/errors.rs b/crates/stim-parser/tests/errors.rs index 6b807747..6f129f7d 100644 --- a/crates/stim-parser/tests/errors.rs +++ b/crates/stim-parser/tests/errors.rs @@ -76,6 +76,24 @@ fn unclosed_bracket_yields_syntax_error() { assert_eq!(err.iter().next().unwrap().code, Some("syntax")); } +#[test] +fn unknown_tag_escape_yields_syntax_error() { + let err = parse(r"H[bad\x] 0").unwrap_err(); + assert_eq!(err.iter().next().unwrap().code, Some("syntax")); +} + +#[test] +fn raw_newline_in_tag_yields_syntax_error() { + let err = parse("H[bad\ntag] 0").unwrap_err(); + assert_eq!(err.iter().next().unwrap().code, Some("syntax")); +} + +#[test] +fn raw_carriage_return_in_tag_yields_syntax_error() { + let err = parse("H[bad\rtag] 0").unwrap_err(); + assert_eq!(err.iter().next().unwrap().code, Some("syntax")); +} + #[test] fn line_numbers_in_errors_are_correct() { let err = parse("X 0\nY 0\nFROBNICATE 0").unwrap_err(); diff --git a/crates/stim-parser/tests/extended.rs b/crates/stim-parser/tests/extended.rs index 19100f58..3f0a9a5f 100644 --- a/crates/stim-parser/tests/extended.rs +++ b/crates/stim-parser/tests/extended.rs @@ -31,13 +31,13 @@ fn vanilla_h_passes_through() { match &p.instructions[0] { ExtendedInstruction::Gate(GateOp { name, - tags, + tag, targets, span, .. }) => { assert_eq!(*name, GateName::H); - assert!(tags.is_empty()); + assert!(tag.is_empty()); assert_eq!(targets, &vec![0]); assert_eq!(span.line(&p.line_map), 1); } @@ -154,10 +154,9 @@ fn repeat_invalid_extended_tag_in_body_errors() { fn lenient_unknown_tag_on_h_passes_through() { let p = parse_ok("H[unrelated] 0\n"); match &p.instructions[0] { - ExtendedInstruction::Gate(GateOp { name, tags, .. }) => { + ExtendedInstruction::Gate(GateOp { name, tag, .. }) => { assert_eq!(*name, GateName::H); - assert_eq!(tags.len(), 1); - assert_eq!(tags[0].name, "unrelated"); + assert_eq!(tag, "unrelated"); } other => panic!("{other:?}"), } @@ -205,9 +204,9 @@ fn s_dag_t_promotes_to_t_dag() { fn s_with_no_tag_is_vanilla_gate() { let p = parse_ok("S 0\n"); match &p.instructions[0] { - ExtendedInstruction::Gate(GateOp { name, tags, .. }) => { + ExtendedInstruction::Gate(GateOp { name, tag, .. }) => { assert_eq!(*name, GateName::S); - assert!(tags.is_empty()); + assert!(tag.is_empty()); } other => panic!("{other:?}"), } @@ -217,43 +216,45 @@ fn s_with_no_tag_is_vanilla_gate() { fn s_dag_with_no_tag_is_vanilla_gate() { let p = parse_ok("S_DAG 0\n"); match &p.instructions[0] { - ExtendedInstruction::Gate(GateOp { name, tags, .. }) => { + ExtendedInstruction::Gate(GateOp { name, tag, .. }) => { assert_eq!(*name, GateName::SDag); - assert!(tags.is_empty()); + assert!(tag.is_empty()); } other => panic!("{other:?}"), } } #[test] -fn s_with_unknown_tag_errors() { - let err = parse_err("S[X] 0\n"); - let d = err.iter().next().unwrap(); - assert_eq!(d.code, Some("invalid-tag")); - assert!( - err.to_string().starts_with("error at line 1,"), - "display: {err}" - ); -} - -#[test] -fn s_dag_with_unknown_tag_errors() { - assert_eq!(err_code("S_DAG[X] 0\n"), Some("invalid-tag")); -} - -#[test] -fn s_with_multiple_tags_errors() { - assert_eq!(err_code("S[T, X] 0\n"), Some("invalid-tag")); -} - -#[test] -fn s_t_with_params_errors() { - assert_eq!(err_code("S[T(0.5)] 0\n"), Some("invalid-tag")); +fn non_t_tags_on_s_and_s_dag_are_ignored() { + for (src, expected_name, expected_tag) in [ + ("S[note: abc] 0\n", GateName::S, "note: abc"), + ("S_DAG[X] 0\n", GateName::SDag, "X"), + ("S[T, X] 0\n", GateName::S, "T, X"), + ("S[T(0.5)] 0\n", GateName::S, "T(0.5)"), + ("S_DAG[T(0.5)] 0\n", GateName::SDag, "T(0.5)"), + (r"S[T\n] 0", GateName::S, "T\n"), + ] { + let p = parse_ok(src); + match &p.instructions[0] { + ExtendedInstruction::Gate(GateOp { name, tag, .. }) => { + assert_eq!(*name, expected_name); + assert_eq!(tag, expected_tag); + } + other => panic!("{src}: {other:?}"), + } + } } #[test] -fn s_dag_t_with_params_errors() { - assert_eq!(err_code("S_DAG[T(0.5)] 0\n"), Some("invalid-tag")); +fn tags_on_native_t_and_t_dag_are_ignored() { + assert!(matches!( + &parse_ok("T[note] 0\n").instructions[0], + ExtendedInstruction::T { targets, .. } if targets == &vec![0] + )); + assert!(matches!( + &parse_ok("T_DAG[T] 1\n").instructions[0], + ExtendedInstruction::TDag { targets, .. } if targets == &vec![1] + )); } #[test] @@ -334,9 +335,9 @@ fn i_u3_missing_phi_errors() { fn i_with_no_tag_is_vanilla_identity() { let p = parse_ok("I 0\n"); match &p.instructions[0] { - ExtendedInstruction::Gate(GateOp { name, tags, .. }) => { + ExtendedInstruction::Gate(GateOp { name, tag, .. }) => { assert_eq!(*name, GateName::Identity); - assert!(tags.is_empty()); + assert!(tag.is_empty()); } other => panic!("{other:?}"), } @@ -403,6 +404,16 @@ fn i_with_unknown_tag_errors() { ); } +#[test] +fn i_with_general_comment_tag_errors() { + assert_eq!(err_code("I[note: abc] 0\n"), Some("invalid-tag")); + + let err = parse_err(r"I[note\nbad] 0"); + let message = &err.iter().next().unwrap().message; + assert!(message.contains(r"note\nbad"), "message: {message}"); + assert!(!message.contains('\n'), "message: {message}"); +} + #[test] fn i_with_multiple_rotation_tags_errors() { let err = parse_err("I[R_X(theta=0.1), R_Y(theta=0.2)] 0\n"); @@ -499,6 +510,7 @@ fn i_error_loss_wrong_arg_count_errors() { #[test] fn i_error_unknown_tag_errors() { assert_eq!(err_code("I_ERROR[bogus](0.1) 0\n"), Some("invalid-tag")); + assert_eq!(err_code(r"I_ERROR[loss\r](0.1) 0"), Some("invalid-tag")); } #[test] diff --git a/crates/stim-parser/tests/gates.rs b/crates/stim-parser/tests/gates.rs index 8b48f365..9629b171 100644 --- a/crates/stim-parser/tests/gates.rs +++ b/crates/stim-parser/tests/gates.rs @@ -14,13 +14,13 @@ fn parse_single_h_one_target() { match &instructions(&p)[0] { Instruction::Gate(GateOp { name, - tags, + tag, args, targets, span, }) => { assert_eq!(*name, GateName::H); - assert!(tags.is_empty()); + assert!(tag.is_empty()); assert!(args.is_empty()); assert_eq!(targets, &[Target::Qubit(0)]); assert_eq!(span.line(&p.line_map), 1); diff --git a/crates/stim-parser/tests/measure.rs b/crates/stim-parser/tests/measure.rs index 52fe10bf..d924e66c 100644 --- a/crates/stim-parser/tests/measure.rs +++ b/crates/stim-parser/tests/measure.rs @@ -144,12 +144,12 @@ fn parse_m_with_two_args_rejected() { fn parse_mpad_no_args_no_tags() { let p = parse("MPAD 0 1 0").unwrap(); let Instruction::MPad { - tags, prob, bits, .. + tag, prob, bits, .. } = &p.instructions[0] else { panic!("{:?}", p.instructions[0]); }; - assert!(tags.is_empty()); + assert!(tag.is_empty()); assert_eq!(*prob, None); assert_eq!(bits, &[0usize, 1, 0]); } diff --git a/crates/stim-parser/tests/proptest_ast.rs b/crates/stim-parser/tests/proptest_ast.rs index a7253800..d4391042 100644 --- a/crates/stim-parser/tests/proptest_ast.rs +++ b/crates/stim-parser/tests/proptest_ast.rs @@ -108,7 +108,7 @@ fn gate_instr() -> impl Strategy { (single_qubit_clifford(), one_q_targets()).prop_map(|(name, targets)| { Instruction::Gate(GateOp { name, - tags: vec![], + tag: String::new(), args: vec![], targets: targets.into_iter().map(Target::Qubit).collect(), span: span0(), @@ -117,7 +117,7 @@ fn gate_instr() -> impl Strategy { (two_qubit_clifford(), two_q_pair_targets()).prop_map(|(name, targets)| { Instruction::Gate(GateOp { name, - tags: vec![], + tag: String::new(), args: vec![], targets: targets.into_iter().map(Target::Qubit).collect(), span: span0(), @@ -130,14 +130,14 @@ fn noise_instr() -> impl Strategy { prop_oneof![ (prob_lit(), one_q_targets()).prop_map(|(p, targets)| Instruction::Noise(NoiseOp { name: NoiseName::Depolarize1, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), })), (prob_lit(), two_q_pair_targets()).prop_map(|(p, targets)| Instruction::Noise(NoiseOp { name: NoiseName::Depolarize2, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -145,7 +145,7 @@ fn noise_instr() -> impl Strategy { (prob_lit(), prob_lit(), prob_lit(), one_q_targets()).prop_map(|(a, b, c, targets)| { Instruction::Noise(NoiseOp { name: NoiseName::PauliChannel1, - tags: vec![], + tag: String::new(), args: vec![a, b, c], targets, span: span0(), @@ -153,21 +153,21 @@ fn noise_instr() -> impl Strategy { }), (prob_lit(), one_q_targets()).prop_map(|(p, targets)| Instruction::Noise(NoiseOp { name: NoiseName::XError, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), })), (prob_lit(), one_q_targets()).prop_map(|(p, targets)| Instruction::Noise(NoiseOp { name: NoiseName::YError, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), })), (prob_lit(), one_q_targets()).prop_map(|(p, targets)| Instruction::Noise(NoiseOp { name: NoiseName::ZError, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -184,7 +184,7 @@ fn measure_instr() -> impl Strategy { (name, proptest::option::of(prob_lit()), one_q_targets()).prop_map(|(name, args, targets)| { Instruction::Measure(MeasureOp { name, - tags: vec![], + tag: String::new(), args: args.map(|p| vec![p]).unwrap_or_default(), targets, span: span0(), @@ -196,12 +196,14 @@ fn annotation_instr() -> impl Strategy { prop_oneof![ Just(Instruction::Annotation(AnnotationOp { kind: AnnotationKind::Tick, + tag: String::new(), args: vec![], targets: vec![], span: span0(), })), Just(Instruction::Annotation(AnnotationOp { kind: AnnotationKind::Detector, + tag: String::new(), args: vec![], targets: vec![], span: span0(), @@ -215,7 +217,7 @@ fn mpad_instr() -> impl Strategy { prop::collection::vec(0usize..2, 1..6), ) .prop_map(|(prob, bits)| Instruction::MPad { - tags: vec![], + tag: String::new(), prob, bits, span: span0(), @@ -238,6 +240,7 @@ fn instr() -> impl Strategy { flat_instr().prop_recursive(1, 6, 2, |_inner| { (1u64..5, prop::collection::vec(flat_instr(), 1..3)).prop_map(|(count, body)| { Instruction::Repeat { + tag: String::new(), count, body, span: span0(), @@ -260,7 +263,7 @@ fn ext_gate_instr() -> impl Strategy { (single_qubit_clifford(), one_q_targets()).prop_map(|(name, targets)| { ExtendedInstruction::Gate(GateOp { name, - tags: vec![], + tag: String::new(), args: vec![], targets: targets.into_iter().map(Target::Qubit).collect(), span: span0(), @@ -269,7 +272,7 @@ fn ext_gate_instr() -> impl Strategy { (two_qubit_clifford(), two_q_pair_targets()).prop_map(|(name, targets)| { ExtendedInstruction::Gate(GateOp { name, - tags: vec![], + tag: String::new(), args: vec![], targets: targets.into_iter().map(Target::Qubit).collect(), span: span0(), @@ -283,7 +286,7 @@ fn ext_noise_instr() -> impl Strategy { (prob_lit(), one_q_targets()).prop_map(|(p, targets)| ExtendedInstruction::Noise( NoiseOp { name: NoiseName::Depolarize1, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -292,7 +295,7 @@ fn ext_noise_instr() -> impl Strategy { (prob_lit(), two_q_pair_targets()).prop_map(|(p, targets)| ExtendedInstruction::Noise( NoiseOp { name: NoiseName::Depolarize2, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -301,7 +304,7 @@ fn ext_noise_instr() -> impl Strategy { (prob_lit(), prob_lit(), prob_lit(), one_q_targets()).prop_map(|(a, b, c, targets)| { ExtendedInstruction::Noise(NoiseOp { name: NoiseName::PauliChannel1, - tags: vec![], + tag: String::new(), args: vec![a, b, c], targets, span: span0(), @@ -310,7 +313,7 @@ fn ext_noise_instr() -> impl Strategy { (prob_lit(), one_q_targets()).prop_map(|(p, targets)| ExtendedInstruction::Noise( NoiseOp { name: NoiseName::XError, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -319,7 +322,7 @@ fn ext_noise_instr() -> impl Strategy { (prob_lit(), one_q_targets()).prop_map(|(p, targets)| ExtendedInstruction::Noise( NoiseOp { name: NoiseName::YError, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -328,7 +331,7 @@ fn ext_noise_instr() -> impl Strategy { (prob_lit(), one_q_targets()).prop_map(|(p, targets)| ExtendedInstruction::Noise( NoiseOp { name: NoiseName::ZError, - tags: vec![], + tag: String::new(), args: vec![p], targets, span: span0(), @@ -346,7 +349,7 @@ fn ext_measure_instr() -> impl Strategy { (name, proptest::option::of(prob_lit()), one_q_targets()).prop_map(|(name, args, targets)| { ExtendedInstruction::Measure(MeasureOp { name, - tags: vec![], + tag: String::new(), args: args.map(|p| vec![p]).unwrap_or_default(), targets, span: span0(), @@ -358,12 +361,14 @@ fn ext_annotation_instr() -> impl Strategy { prop_oneof![ Just(ExtendedInstruction::Annotation(AnnotationOp { kind: AnnotationKind::Tick, + tag: String::new(), args: vec![], targets: vec![], span: span0(), })), Just(ExtendedInstruction::Annotation(AnnotationOp { kind: AnnotationKind::Detector, + tag: String::new(), args: vec![], targets: vec![], span: span0(), @@ -436,7 +441,7 @@ fn ext_flat() -> impl Strategy { prop::collection::vec(any::(), 1..6), ) .prop_map(|(prob, bits)| ExtendedInstruction::MPad { - tags: vec![], + tag: String::new(), prob, bits, span: span0(), @@ -448,6 +453,7 @@ fn ext_instr() -> impl Strategy { ext_flat().prop_recursive(1, 6, 2, |_inner| { (1u64..5, prop::collection::vec(ext_flat(), 1..3)).prop_map(|(count, body)| { ExtendedInstruction::Repeat { + tag: String::new(), count, body, span: span0(), diff --git a/crates/stim-parser/tests/syntax.rs b/crates/stim-parser/tests/syntax.rs index 2d4ce1e8..b48722ff 100644 --- a/crates/stim-parser/tests/syntax.rs +++ b/crates/stim-parser/tests/syntax.rs @@ -61,7 +61,9 @@ fn parse_simple_repeat() { let p = parse(src).unwrap(); assert_eq!(p.instructions.len(), 1); match &p.instructions[0] { - Instruction::Repeat { count, body, span } => { + Instruction::Repeat { + count, body, span, .. + } => { assert_eq!(*count, 3); assert_eq!(body.len(), 2); assert_eq!(span.line(&p.line_map), 1); diff --git a/crates/stim-parser/tests/tags.rs b/crates/stim-parser/tests/tags.rs index 04c7ab76..d759da2d 100644 --- a/crates/stim-parser/tests/tags.rs +++ b/crates/stim-parser/tests/tags.rs @@ -11,38 +11,38 @@ fn approx_eq(a: f64, b: f64) { fn parse_tag_bare_ident() { let p = parse("S[T] 0").unwrap(); match &p.instructions[0] { - Instruction::Gate(GateOp { name, tags, .. }) => { + Instruction::Gate(GateOp { name, tag, .. }) => { assert_eq!(*name, GateName::S); - assert_eq!(tags.len(), 1); - assert_eq!(tags[0].name, "T"); - assert!(tags[0].params.is_empty()); + assert_eq!(tag, "T"); } other => panic!("{other:?}"), } } +#[test] +fn empty_and_absent_tags_are_empty_strings() { + let p = parse("H[] 0\nH 1").unwrap(); + for instruction in &p.instructions { + let Instruction::Gate(GateOp { tag, .. }) = instruction else { + panic!("{instruction:?}"); + }; + assert!(tag.is_empty()); + } + assert_eq!(p.to_string(), "H 0\nH 1\n"); +} + #[test] fn parse_tag_named_param_pi_expr() { - let half_pi = 0.5 * std::f64::consts::PI; - for src in [ - "I[R_X(theta=0.5*pi)] 0", - "I[R_X(theta=0.5 * pi)] 0", - "I[R_X(theta=0.5pi)] 0", + for (src, expected) in [ + ("I[R_X(theta=0.5*pi)] 0", "R_X(theta=0.5*pi)"), + ("I[R_X(theta=0.5 * pi)] 0", "R_X(theta=0.5 * pi)"), + ("I[R_X(theta=0.5pi)] 0", "R_X(theta=0.5pi)"), ] { let p = parse(src).unwrap_or_else(|e| panic!("{src}: {e:?}")); match &p.instructions[0] { - Instruction::Gate(GateOp { name, tags, .. }) => { + Instruction::Gate(GateOp { name, tag, .. }) => { assert_eq!(*name, GateName::Identity); - assert_eq!(tags.len(), 1); - assert_eq!(tags[0].name, "R_X"); - match &tags[0].params[..] { - [TagParam::Named { key, value, had_pi }] => { - assert_eq!(key, "theta"); - approx_eq(*value, half_pi); - assert!(*had_pi); - } - other => panic!("{src}: {other:?}"), - } + assert_eq!(tag, expected); } other => panic!("{src}: {other:?}"), } @@ -53,16 +53,8 @@ fn parse_tag_named_param_pi_expr() { fn parse_tag_u3_three_named_params() { let p = parse("I[U3(theta=0.34*pi, phi=0.21*pi, lambda=0.46*pi)] 5").unwrap(); match &p.instructions[0] { - Instruction::Gate(GateOp { tags, .. }) => { - let params = &tags[0].params; - assert_eq!(params.len(), 3); - // Order is preserved. - for (param, expected_key) in params.iter().zip(["theta", "phi", "lambda"]) { - let TagParam::Named { key, .. } = param else { - panic!("expected named, got {param:?}"); - }; - assert_eq!(key, expected_key); - } + Instruction::Gate(GateOp { tag, .. }) => { + assert_eq!(tag, "U3(theta=0.34*pi, phi=0.21*pi, lambda=0.46*pi)") } other => panic!("{other:?}"), } @@ -73,16 +65,10 @@ fn parse_loss_tag_with_args() { let p = parse("I_ERROR[loss](1.0) 0").unwrap(); match &p.instructions[0] { Instruction::Noise(NoiseOp { - name, tags, args, .. + name, tag, args, .. }) => { assert_eq!(*name, NoiseName::IError); - assert_eq!( - tags, - &[Tag { - name: "loss".into(), - params: vec![] - }] - ); + assert_eq!(tag, "loss"); approx_eq(args[0], 1.0); } other => panic!("{other:?}"), @@ -94,12 +80,9 @@ fn parse_correlated_loss_three_args() { let p = parse("I_ERROR[correlated_loss](0.1, 0.2, 0.3) 0 1").unwrap(); match &p.instructions[0] { Instruction::Noise(NoiseOp { - tags, - args, - targets, - .. + tag, args, targets, .. }) => { - assert_eq!(tags[0].name, "correlated_loss"); + assert_eq!(tag, "correlated_loss"); assert_eq!(args.len(), 3); assert_eq!(targets, &[0, 1]); } @@ -108,28 +91,80 @@ fn parse_correlated_loss_three_args() { } #[test] -fn parse_multi_tag() { - // Stim supports comma-separated multiple tags inside `[…]`. +fn commas_are_tag_content() { let p = parse("S[T,debug] 0").unwrap(); match &p.instructions[0] { - Instruction::Gate(GateOp { tags, .. }) => { - assert_eq!(tags.len(), 2); - assert_eq!(tags[0].name, "T"); - assert_eq!(tags[1].name, "debug"); - } + Instruction::Gate(GateOp { tag, .. }) => assert_eq!(tag, "T,debug"), other => panic!("{other:?}"), } } #[test] -fn parse_tag_positional_floats() { - // `[R_X(0.5)]` — tag param without a key is positional. +fn structured_looking_tag_remains_opaque() { let p = parse("I[R_X(0.5)] 0").unwrap(); match &p.instructions[0] { - Instruction::Gate(GateOp { tags, .. }) => match &tags[0].params[..] { - [TagParam::Positional(v)] => approx_eq(*v, 0.5), - other => panic!("{other:?}"), - }, + Instruction::Gate(GateOp { tag, .. }) => assert_eq!(tag, "R_X(0.5)"), other => panic!("{other:?}"), } } + +#[test] +fn arbitrary_stim_tag_is_one_opaque_string() { + let p = parse("H[x^0*y^0,horizontal,(note),a[b,λ] 0").unwrap(); + assert_eq!(p.to_string(), "H[x^0*y^0,horizontal,(note),a[b,λ] 0\n"); +} + +#[test] +fn annotation_and_repeat_tags_are_preserved() { + let p = parse("REPEAT[loop note] 2 { TICK[SE reset] }").unwrap(); + assert_eq!( + p.to_string(), + "REPEAT[loop note] 2 {\n TICK[SE reset]\n}\n" + ); +} + +#[test] +fn stim_tag_escapes_decode_and_print_canonically() { + let p = parse(r"H[test \B\C\r\n] 0").unwrap(); + assert_eq!(p.to_string(), "H[test \\B\\C\\r\\n] 0\n"); +} + +#[test] +fn opaque_tags_round_trip_on_every_instruction_family() { + let encoded = r"meta, [λ\B\C\n\r"; + let decoded = "meta, [λ\\]\n\r"; + let src = format!( + "H[{encoded}] 0\n\ + X_ERROR[{encoded}](0.1) 0\n\ + M[{encoded}] 0\n\ + TICK[{encoded}]\n\ + MPP[{encoded}] X0\n\ + MPAD[{encoded}] 0\n\ + REPEAT[{encoded}] 1 {{ H 0 }}" + ); + + let parsed = parse(&src).unwrap(); + let tags = |program: &Program| { + program + .instructions + .iter() + .map(|instruction| match instruction { + Instruction::Gate(op) => &op.tag, + Instruction::Noise(op) => &op.tag, + Instruction::Measure(op) => &op.tag, + Instruction::Annotation(op) => &op.tag, + Instruction::Mpp(op) => &op.tag, + Instruction::MPad { tag, .. } | Instruction::Repeat { tag, .. } => tag, + }) + .cloned() + .collect::>() + }; + assert_eq!(tags(&parsed), vec![decoded.to_string(); 7]); + + let printed = parsed.to_string(); + assert_eq!(printed.matches(&format!("[{encoded}]")).count(), 7); + assert_eq!( + tags(&parse(&printed).unwrap()), + vec![decoded.to_string(); 7] + ); +} diff --git a/ppvm-python/test/generalized_tableau/test_stim.py b/ppvm-python/test/generalized_tableau/test_stim.py index 08996fc0..599264b7 100644 --- a/ppvm-python/test/generalized_tableau/test_stim.py +++ b/ppvm-python/test/generalized_tableau/test_stim.py @@ -170,7 +170,12 @@ def test_run_stim_string_tick_and_annotations_are_noops(): # Note: rec[-1] targets on annotations are silently dropped during # parsing; the annotation itself is preserved as a no-op. circuit = ( - "QUBIT_COORDS(0, 0) 0\nX 0\nTICK\nM 0\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-1]\n" + "QUBIT_COORDS[x^0*y^0,horizontal,block=1](5.5, 0) 0\n" + "X 0\n" + "TICK[SE reset]\n" + "M 0\n" + "DETECTOR rec[-1]\n" + "OBSERVABLE_INCLUDE(0) rec[-1]\n" ) tab = GeneralizedTableau(1) results = tab.run(StimProgram.parse(circuit))