Skip to main content

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}