Skip to main content

bloq_ir/
key.rs

1//! Compile-stable node identity.
2//!
3//! A [`BloqNodeId`] identifies a node inside one program, and nothing more:
4//! two programs compiled from the same block graph — the ordinary compile and
5//! a Clifford proxy of it, say — number their nodes independently, and an edit
6//! may recycle a vacant slot (SEM-ID). A consumer that has to line the two up
7//! needs an identity derived from what a node *is*, not where it landed.
8//!
9//! [`NodeKey`] is that identity: a node's [`NodeProvenance`] in canonical
10//! form, plus an ordinal that separates the nodes sharing one provenance. Both
11//! halves are compile-stable — provenance names source constructs rather than
12//! compiled artifacts, and the ordinal comes from
13//! [`SubGraph::deterministic_emit_order`], which is fixed by graph shape.
14//!
15//! What has no key is a node with no provenance: a region node, or a
16//! hand-built synthetic node. Without source provenance these nodes have no
17//! cross-program identity.
18
19use std::collections::BTreeMap;
20use std::fmt;
21
22use crate::{
23    Basis, Bloq, BloqNode, BloqNodeId, CycleDetected, NodeProvenance, PortRole, SubGraph,
24    TemporalPipeRef,
25};
26
27/// A node identity that survives recompilation and renumbering.
28///
29/// Ordered and hashable so it can key a lookup table; the ordering is an
30/// implementation detail of the provenance encoding and carries no meaning.
31/// [`Display`](fmt::Display) renders it the way the `.bloqir` text format
32/// spells provenance, suffixed with `#ordinal`.
33#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct NodeKey {
35    provenance: KeyProvenance,
36    ordinal: u32,
37}
38
39impl NodeKey {
40    /// This node's index among the nodes of its level that share its
41    /// provenance, in [`SubGraph::deterministic_emit_order`].
42    ///
43    /// Always `0` on a key from [`BloqNode::stable_key`], which sees one node
44    /// and so cannot count its peers; [`SubGraph::stable_keys`] assigns the
45    /// real ordinals.
46    #[must_use]
47    pub fn ordinal(&self) -> u32 {
48        self.ordinal
49    }
50}
51
52/// Canonical provenance: the payload of [`NodeProvenance`] reduced to plain
53/// ordered data, so a key can derive `Ord`/`Hash`.
54///
55/// Block members are sorted because a node's identity is the *set* of source
56/// blocks it fused, not the order lowering happened to list them in.
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
58enum KeyProvenance {
59    Blocks(Vec<[i32; 3]>),
60    Pipe {
61        src: [i32; 3],
62        dst: [i32; 3],
63        hadamard: bool,
64    },
65    SpatialPort {
66        source: [i32; 3],
67        role: PortRole,
68    },
69    Padding {
70        src: [i32; 3],
71        dst: [i32; 3],
72        rounds: u32,
73    },
74    Generator(u32),
75    Action(u32),
76    BranchSelector(String),
77    OutputFrame {
78        port: [i32; 3],
79        basis: Basis,
80    },
81}
82
83/// Mirrors [`NodeProvenance`]'s `Display`, so a key and the program view name
84/// the same node alike.
85impl fmt::Display for NodeKey {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        let coord = |f: &mut fmt::Formatter<'_>, [x, y, z]: [i32; 3]| write!(f, "({x},{y},{z})");
88        match &self.provenance {
89            KeyProvenance::Blocks(blocks) => {
90                write!(f, "blocks")?;
91                for &block in blocks {
92                    write!(f, " ")?;
93                    coord(f, block)?;
94                }
95                Ok(())
96            }
97            KeyProvenance::Pipe { src, dst, hadamard } => {
98                write!(f, "pipe ")?;
99                coord(f, *src)?;
100                write!(f, ">")?;
101                coord(f, *dst)?;
102                if *hadamard {
103                    write!(f, " H")?;
104                }
105                Ok(())
106            }
107            KeyProvenance::SpatialPort { source, role } => {
108                write!(f, "spatial-port {} ", role.as_str())?;
109                coord(f, *source)
110            }
111            KeyProvenance::Padding { src, dst, rounds } => {
112                write!(f, "padding ")?;
113                coord(f, *src)?;
114                write!(f, ">")?;
115                coord(f, *dst)?;
116                write!(f, " rounds {rounds}")
117            }
118            KeyProvenance::Generator(ordinal) => write!(f, "generator {ordinal}"),
119            KeyProvenance::Action(ordinal) => write!(f, "action {ordinal}"),
120            KeyProvenance::BranchSelector(name) => write!(f, "selector {name}"),
121            KeyProvenance::OutputFrame { port, basis } => {
122                let basis = match basis {
123                    Basis::X => "x",
124                    Basis::Z => "z",
125                };
126                write!(f, "frame {basis} ")?;
127                coord(f, *port)
128            }
129        }?;
130        write!(f, "#{}", self.ordinal)
131    }
132}
133
134fn pipe_key(pipe: &TemporalPipeRef) -> ([i32; 3], [i32; 3]) {
135    (pipe.src.to_array(), pipe.dst.to_array())
136}
137
138impl BloqNode {
139    /// This node's provenance-derived key, at ordinal `0`.
140    ///
141    /// `None` for a node with no provenance (a region node, or a hand-built
142    /// synthetic one), which has no cross-program identity to report.
143    ///
144    /// Use [`SubGraph::stable_keys`] when nodes may share a provenance: the
145    /// ordinal that separates them is a property of the level, not of any one
146    /// node. Memory padding is the case that needs it — several padding nodes
147    /// can idle on one pipe for the same number of rounds.
148    #[must_use]
149    pub fn stable_key(&self) -> Option<NodeKey> {
150        let provenance = match &self.provenance {
151            NodeProvenance::BlockComponent { members } => {
152                let mut blocks: Vec<_> =
153                    members.iter().map(|member| member.pos.to_array()).collect();
154                blocks.sort_unstable();
155                KeyProvenance::Blocks(blocks)
156            }
157            NodeProvenance::TemporalPipe { pipe } => {
158                let (src, dst) = pipe_key(pipe);
159                KeyProvenance::Pipe {
160                    src,
161                    dst,
162                    hadamard: pipe.hadamard,
163                }
164            }
165            NodeProvenance::SpatialPortSubstitution { source, role } => {
166                KeyProvenance::SpatialPort {
167                    source: source.to_array(),
168                    role: *role,
169                }
170            }
171            NodeProvenance::MemoryPadding { pipe, rounds } => {
172                let (src, dst) = pipe_key(pipe);
173                KeyProvenance::Padding {
174                    src,
175                    dst,
176                    rounds: *rounds,
177                }
178            }
179            NodeProvenance::Generator { ordinal } => KeyProvenance::Generator(*ordinal),
180            NodeProvenance::Action { ordinal } => KeyProvenance::Action(*ordinal),
181            NodeProvenance::BranchSelector { name } => KeyProvenance::BranchSelector(name.clone()),
182            NodeProvenance::OutputFrame { port, basis } => KeyProvenance::OutputFrame {
183                port: port.to_array(),
184                basis: *basis,
185            },
186            NodeProvenance::None => return None,
187        };
188        Some(NodeKey {
189            provenance,
190            ordinal: 0,
191        })
192    }
193}
194
195impl SubGraph {
196    /// This level's provenance-bearing nodes with their stable keys, in
197    /// [`SubGraph::deterministic_emit_order`].
198    ///
199    /// Nodes sharing a provenance are numbered in that order, which is what
200    /// makes the keys line up across two programs: the schedule follows graph
201    /// shape, so corresponding nodes are reached in corresponding positions.
202    /// Nodes with no provenance are skipped, not keyed.
203    ///
204    /// # Errors
205    ///
206    /// Returns [`CycleDetected`] when the level has no emit order to number in.
207    pub fn stable_keys(&self) -> Result<Vec<(NodeKey, BloqNodeId)>, CycleDetected> {
208        let mut ordinals: BTreeMap<KeyProvenance, u32> = BTreeMap::new();
209        let mut keys = Vec::new();
210        for id in self.deterministic_emit_order()? {
211            let Some(key) = self[id].stable_key() else {
212                continue;
213            };
214            let next = ordinals.entry(key.provenance.clone()).or_default();
215            let ordinal = *next;
216            *next += 1;
217            keys.push((
218                NodeKey {
219                    provenance: key.provenance,
220                    ordinal,
221                },
222                id,
223            ));
224        }
225        Ok(keys)
226    }
227}
228
229impl Bloq {
230    /// The top level's [`stable keys`](SubGraph::stable_keys) as a lookup
231    /// table, for resolving a key obtained from another program into a node id
232    /// in this one.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`CycleDetected`] when the program has no emit order.
237    pub fn stable_key_map(&self) -> Result<BTreeMap<NodeKey, BloqNodeId>, CycleDetected> {
238        Ok(self.top().stable_keys()?.into_iter().collect())
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    /// A block, a temporal pipe, two padding nodes idling the *same* pipe for
247    /// the same number of rounds, and a terminal block — the shape that made
248    /// block membership alone an unusable identity, since padding nodes are
249    /// spliced into a pipe and carry no blocks at all.
250    ///
251    /// `base` shifts every node id without touching any provenance: the
252    /// stand-in for "the same graph, compiled a second time".
253    fn program(base: u32) -> Bloq {
254        let (a, b, c, d, e) = (base, base + 1, base + 2, base + 3, base + 4);
255        Bloq::from_text(&format!(
256            "\
257BLOQIR 1
258
259graph {{
260  n{a} quantum {{
261    from blocks (1,0,0) (0,0,0)
262  }}
263  n{b} quantum {{
264    from pipe (0,0,0)>(0,0,1)
265  }}
266  n{c} quantum {{
267    from padding (0,0,1)>(0,0,2) rounds 4
268  }}
269  n{d} quantum {{
270    from padding (0,0,1)>(0,0,2) rounds 4
271  }}
272  n{e} quantum {{
273    from blocks (0,0,2)
274  }}
275  n{a} -> n{b} order
276  n{b} -> n{c} order
277  n{c} -> n{d} order
278  n{d} -> n{e} order
279}}
280"
281        ))
282        .expect("valid .bloqir text")
283    }
284
285    #[test]
286    fn keys_name_provenance_and_disambiguate_with_an_ordinal() {
287        let program = program(90);
288
289        let rendered: Vec<_> = program
290            .top()
291            .stable_keys()
292            .expect("acyclic program")
293            .into_iter()
294            .map(|(key, id)| (key.to_string(), id))
295            .collect();
296
297        assert_eq!(
298            rendered,
299            [
300                ("blocks (0,0,0) (1,0,0)#0".to_owned(), BloqNodeId(90)),
301                ("pipe (0,0,0)>(0,0,1)#0".to_owned(), BloqNodeId(91)),
302                (
303                    "padding (0,0,1)>(0,0,2) rounds 4#0".to_owned(),
304                    BloqNodeId(92)
305                ),
306                (
307                    "padding (0,0,1)>(0,0,2) rounds 4#1".to_owned(),
308                    BloqNodeId(93)
309                ),
310                ("blocks (0,0,2)#0".to_owned(), BloqNodeId(94)),
311            ],
312            "members sort, and the two padding nodes separate by ordinal"
313        );
314    }
315
316    /// The point of the whole module: renumbering must not change a key.
317    #[test]
318    fn keys_survive_renumbering() {
319        let plain = program(0).stable_key_map().expect("keyed");
320        let shifted = program(90).stable_key_map().expect("keyed");
321
322        let keys: Vec<_> = plain.keys().collect();
323        assert_eq!(
324            keys,
325            shifted.keys().collect::<Vec<_>>(),
326            "the same source constructs key the same way in both programs"
327        );
328        // The ids behind them did move, which is what makes the keys earn
329        // their keep.
330        assert_eq!(plain[keys[0]], BloqNodeId(0));
331        assert_eq!(shifted[keys[0]], BloqNodeId(90));
332    }
333
334    #[test]
335    fn key_map_rejects_a_cyclic_program() {
336        let mut program = program(0);
337        program.add_edge(BloqNodeId(4), BloqNodeId(0), crate::BloqEdge::Order);
338
339        assert_eq!(program.stable_key_map(), Err(CycleDetected));
340    }
341
342    #[test]
343    fn provenance_free_nodes_have_no_key() {
344        let program = Bloq::from_text(
345            "\
346BLOQIR 1
347
348graph {
349  n0 rus in0 {
350    body {
351    }
352  }
353  n1 quantum {
354    from blocks (0,0,0)
355  }
356}
357",
358        )
359        .expect("valid .bloqir text");
360
361        assert_eq!(program[BloqNodeId(0)].stable_key(), None);
362        assert_eq!(
363            program
364                .stable_key_map()
365                .expect("keyed")
366                .into_values()
367                .collect::<Vec<_>>(),
368            [BloqNodeId(1)],
369            "the region has no key"
370        );
371    }
372}