bloq_stim/dialect.rs
1//! The `HONEST_T` convention: how a non-Clifford `T` survives a round trip
2//! through Stim, which has no such gate.
3//!
4//! Bloq's own circuit text is *Clifft* — Stim's syntax plus the non-Clifford
5//! `T`/`T_DAG` gates and `EXP_VAL` probes that the Clifft simulator
6//! understands. Stim itself rejects both. Rather than lose the T gates,
7//! emission in the [`StimDialect::Stim`] dialect writes each one as the
8//! Clifford `S` that shares its axis, tagged `HONEST_T`. The tag is inert to
9//! Stim (it round-trips through parse/print) and identifies exactly the
10//! instructions to turn back into `T` on the way out, so the two dialects name
11//! the same circuit.
12//!
13//! Owning both directions here is the point: the convention is one table, and
14//! a reader and a writer that disagree about it silently corrupt a circuit.
15
16/// The two spellings emission can produce for a non-Clifford `T` gate.
17///
18/// The dialects differ *only* in how `T`/`T_DAG` are written; every other
19/// instruction is identical, so a circuit with no non-Clifford gate emits
20/// byte-identically in both.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
22pub enum StimDialect {
23 /// Stim proper: `T` becomes `S[HONEST_T]` and `T_DAG` becomes
24 /// `S_DAG[HONEST_T]`. The default, because it is the only dialect Stim can
25 /// parse.
26 #[default]
27 Stim,
28 /// Clifft: `T`/`T_DAG` are written literally. Stim cannot parse the
29 /// result; the Clifft simulator can.
30 Clifft,
31}
32
33/// The `(stim gate, clifft gate)` pairs the convention maps, longest first so
34/// a prefix match cannot mistake `T_DAG` for `T`.
35const HONEST_T_GATES: [(&str, &str); 2] = [("S_DAG", "T_DAG"), ("S", "T")];
36
37/// The tag marking an `S` that is really a `T`.
38pub const HONEST_T_TAG: &str = "HONEST_T";
39
40/// Rewrite Clifft circuit text so Stim can parse it.
41///
42/// Two changes, both forced by Stim's grammar: `T`/`T_DAG` become
43/// `S[HONEST_T]`/`S_DAG[HONEST_T]`, and **`EXP_VAL` lines are dropped**
44/// entirely — they are a Clifft-only probe instruction with no Stim
45/// equivalent, so there is nothing to translate them into. That makes the
46/// conversion lossy in exactly one way; [`stim_to_clifft_text`] recovers the
47/// gates but not the probes.
48///
49/// Only a gate that opens a line (after indentation) and is followed by its
50/// target list is rewritten, so an already-tagged or otherwise decorated `T`
51/// is left alone rather than silently re-spelled.
52///
53/// # Examples
54///
55/// ```
56/// # use bloq_stim::clifft_to_stim_text;
57/// let clifft = "H 0\nREPEAT 2 {\n T_DAG 1\n}\nEXP_VAL X0*Z1\n";
58///
59/// assert_eq!(
60/// clifft_to_stim_text(clifft),
61/// "H 0\nREPEAT 2 {\n S_DAG[HONEST_T] 1\n}\n",
62/// );
63/// ```
64pub fn clifft_to_stim_text(text: &str) -> String {
65 let mut out = String::with_capacity(text.len());
66 for line in text.lines() {
67 let content = line.trim_start();
68 let indent = &line[..line.len() - content.len()];
69 if content.starts_with("EXP_VAL ") {
70 continue;
71 }
72 out.push_str(indent);
73 match HONEST_T_GATES
74 .iter()
75 .find(|(_, clifft)| opens_instruction(content, clifft))
76 {
77 Some((stim, clifft)) => {
78 out.push_str(stim);
79 out.push('[');
80 out.push_str(HONEST_T_TAG);
81 out.push(']');
82 out.push_str(&content[clifft.len()..]);
83 }
84 None => out.push_str(content),
85 }
86 out.push('\n');
87 }
88 // `lines` drops the final terminator; a text that did not end in one keeps
89 // that shape so the conversion does not invent a line.
90 if !text.ends_with('\n') && out.ends_with('\n') {
91 out.pop();
92 }
93 out
94}
95
96/// Rewrite Stim circuit text back into Clifft: every `S[HONEST_T]` and
97/// `S_DAG[HONEST_T]` becomes the `T`/`T_DAG` it stands for.
98///
99/// The inverse of [`clifft_to_stim_text`] up to that function's `EXP_VAL`
100/// loss. Untagged `S` gates are genuine Cliffords and are left alone — which
101/// is the whole reason the tag exists.
102///
103/// # Examples
104///
105/// ```
106/// # use bloq_stim::stim_to_clifft_text;
107/// // The untagged `S` is a real Clifford and survives untouched.
108/// assert_eq!(
109/// stim_to_clifft_text("S 0\nS_DAG[HONEST_T] 1\n"),
110/// "S 0\nT_DAG 1\n",
111/// );
112/// ```
113pub fn stim_to_clifft_text(text: &str) -> String {
114 let mut out = String::with_capacity(text.len());
115 for line in text.split_inclusive('\n') {
116 let content = line.trim_start();
117 let indent = &line[..line.len() - content.len()];
118 out.push_str(indent);
119 let replacement = HONEST_T_GATES.iter().find_map(|(stim, clifft)| {
120 let rest = content
121 .strip_prefix(stim)?
122 .strip_prefix('[')?
123 .strip_prefix(HONEST_T_TAG)?
124 .strip_prefix(']')?;
125 rest.starts_with(' ').then_some((*clifft, rest))
126 });
127 match replacement {
128 Some((clifft, rest)) => {
129 out.push_str(clifft);
130 out.push_str(rest);
131 }
132 None => out.push_str(content),
133 }
134 }
135 out
136}
137
138/// Whether `content` opens with `gate` used as an instruction mnemonic — the
139/// bare name followed by its target list, not a longer name (`T_XZ`, `TICK`)
140/// that merely starts with the same letters, and not a decorated one
141/// (`T[TAG]`) whose spelling the caller chose deliberately.
142fn opens_instruction(content: &str, gate: &str) -> bool {
143 content
144 .strip_prefix(gate)
145 .is_some_and(|rest| rest.starts_with(' '))
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn honest_t_round_trips_through_stim() {
154 let clifft = "QUBIT_COORDS(0, 0) 0\nT 0\nREPEAT 3 {\n T_DAG 0\n S 0\n}\nM 0\n";
155
156 let stim = clifft_to_stim_text(clifft);
157
158 assert!(stim.contains("S[HONEST_T] 0"));
159 assert!(stim.contains(" S_DAG[HONEST_T] 0"), "indent preserved");
160 assert_eq!(stim_to_clifft_text(&stim), clifft);
161 }
162
163 #[test]
164 fn only_tagged_s_gates_become_t_gates() {
165 // `S_DAG` is checked before `S`, and a plain `S` is a real Clifford.
166 assert_eq!(stim_to_clifft_text("S 0\nS_DAG 1\n"), "S 0\nS_DAG 1\n");
167 }
168
169 #[test]
170 fn reverse_conversion_only_rewrites_instruction_mnemonics() {
171 let stim = "# S[HONEST_T] is documentation\nS[HONEST_T] 0\n S_DAG[HONEST_T] 1\n";
172
173 assert_eq!(
174 stim_to_clifft_text(stim),
175 "# S[HONEST_T] is documentation\nT 0\n T_DAG 1\n"
176 );
177 }
178
179 /// A gate name that merely starts with `T` is not a `T` gate.
180 #[test]
181 fn longer_gate_names_are_not_mistaken_for_t() {
182 let clifft = "T_XZ 0\nTICK\n";
183
184 assert_eq!(clifft_to_stim_text(clifft), clifft);
185 }
186
187 #[test]
188 fn conversion_preserves_the_absence_of_a_trailing_newline() {
189 assert_eq!(clifft_to_stim_text("T 0"), "S[HONEST_T] 0");
190 assert_eq!(clifft_to_stim_text("T 0\n"), "S[HONEST_T] 0\n");
191 assert_eq!(clifft_to_stim_text(""), "");
192 }
193}