Skip to main content

dbsp/circuit/
operator_traits.rs

1//! Common traits for DBSP operators.
2//!
3//! Operators are the building blocks of DBSP circuits.  An operator
4//! consumes one or more input streams and produces an output stream.
5
6#![allow(async_fn_in_trait)]
7
8use arc_swap::ArcSwap;
9use feldera_storage::{FileCommitter, StoragePath};
10
11use crate::Error;
12use crate::{
13    circuit::{
14        OwnershipPreference, Scope,
15        metadata::{OperatorLocation, OperatorMeta},
16    },
17    trace::cursor::Position,
18};
19use std::borrow::Cow;
20use std::fmt::Display;
21use std::sync::Arc;
22
23use super::GlobalNodeId;
24
25/// Minimal requirements for values exchanged by operators.
26pub trait Data: Clone + 'static {}
27
28impl<T: Clone + 'static> Data for T {}
29
30/// Trait that must be implemented by all operators.
31pub trait Operator: 'static {
32    /// Human-readable operator name for debugging purposes.
33    fn name(&self) -> Cow<'static, str>;
34
35    /// The location the operator was created at
36    fn location(&self) -> OperatorLocation {
37        None
38    }
39
40    /// Initialize the operator
41    fn init(&mut self, _global_id: &GlobalNodeId) {}
42
43    /// Collects metadata about the current operator
44    fn metadata(&self, _meta: &mut OperatorMeta) {}
45
46    /// Notify the operator about the start of a new clock epoch.
47    ///
48    /// `clock_start` and `clock_end` methods support the nested circuit
49    /// architecture.  A nested circuit (or subcircuit) is a node in
50    /// the parent circuit that contains another circuit.  The nested circuit
51    /// has its own clock.  Each parent clock tick starts a new child clock
52    /// epoch.  Each operator gets notified about start and end of a clock
53    /// epoch in its local circuit and all of its ancestors.
54    ///
55    /// Formally, operators in a nested circuit operate over nested streams,
56    /// or streams of streams, with each nested clock epoch starting a new
57    /// stream.  Thus the `clock_start` and `clock_end` methods signal
58    /// respectively the start and completion of a nested stream.
59    ///
60    /// # Examples
61    ///
62    /// For example, feeding the following matrix, where rows represent nested
63    /// streams,
64    ///
65    /// ```text
66    /// ┌       ┐
67    /// │1 2    │
68    /// │3 4 5 6│
69    /// │7 8 9  |
70    /// └       ┘
71    /// ```
72    ///
73    /// to an operator requires the following sequence of invocations
74    ///
75    /// ```text
76    /// clock_start(1) // Start outer clock.
77    /// clock_start(0) // Start nested clock (first row of the matrix).
78    /// eval(1)
79    /// eval(2)
80    /// clock_end(0)   // End nested clock.
81    /// clock_start(0) // Start nested clock (second row).
82    /// eval(3)
83    /// eval(4)
84    /// eval(5)
85    /// eval(6)
86    /// clock_end(0)   // End nested clock.
87    /// clock_start(0) // Start nested clock (third row).
88    /// eval(7)
89    /// eval(8)
90    /// eval(9)
91    /// clock_end(0)   // End nested clock.
92    /// clock_end(1)   // End outer clock.
93    /// ```
94    ///
95    /// Note that the input and output of most operators belong to the same
96    /// clock domain, i.e., an operator cannot consume a single value and
97    /// produce a stream, or the other way around.  The only exception are
98    /// [`ImportOperator`]s that make the contents of a stream in the parent
99    /// circuit available inside a subcircuit.
100    ///
101    /// An operator can have multiple input streams, all of which belong to the
102    /// same clock domain and therefore start and end at the same time.  Hence
103    /// `clock_start` and `clock_end` apply to all input and output streams of
104    /// the operator.
105    ///
106    /// # Arguments
107    ///
108    /// * `scope` - the scope whose clock is restarting.
109    fn clock_start(&mut self, _scope: Scope) {}
110    fn clock_end(&mut self, _scope: Scope) {}
111
112    /// Returns `true` if `self` is an asynchronous operator.
113    ///
114    /// An asynchronous operator may need to wait for external inputs, i.e.,
115    /// inputs from outside the circuit.  While a regular synchronous
116    /// operator is ready to be triggered as soon as all of its input
117    /// streams contain data, an async operator may require additional
118    /// inputs that arrive asynchronously with respect to the operation of
119    /// the circuit (e.g., from an I/O device or via an IPC channel).
120    ///
121    /// We do not allow operators to block, therefore the scheduler must not
122    /// schedule an async operator until it has all external inputs
123    /// available.  The scheduler checks that the operator is ready to
124    /// execute using the [`ready`](`Self::ready`) method.
125    fn is_async(&self) -> bool {
126        false
127    }
128
129    /// Returns `true` if `self` is an input operator.
130    ///
131    /// An input operator feeds new data into the circuit. Examples are
132    /// the `Input` and `Generator` operators.
133    fn is_input(&self) -> bool {
134        false
135    }
136
137    /// Returns `true` if `self` has received all required external inputs and
138    /// is ready to run.
139    ///
140    /// This method must always returns `true` for synchronous operators.  For
141    /// an asynchronous operator, it returns `true` if the operator has all
142    /// external inputs available (see [`is_async`](`Self::is_async`)
143    /// documentation).  Once the operator is ready, it remains ready within
144    /// the current clock cycle, thus the scheduler can safely evaluate the
145    /// operator.
146    fn ready(&self) -> bool {
147        true
148    }
149
150    /// Register callback to be invoked when an asynchronous operator becomes
151    /// ready.
152    ///
153    /// This method should only be used for asynchronous operators (see
154    /// documentation for [`is_async`](`Self::is_async`) and
155    /// [`ready`](`Self::ready`)) in order to enable dynamic schedulers to
156    /// run async operators as they become ready without continuously
157    /// polling them.  The operator need only support this method being
158    /// called once, to set a single callback.
159    ///
160    /// Once the callback has been registered, the operator will invoke the
161    /// callback at every clock cycle, when the operator becomes ready.
162    /// The callback is invoked with at-least-once semantics, meaning that
163    /// spurious invocations are possible.  The scheduler must always check
164    /// if the operator is ready to run by calling [`ready`](`Self::ready`)
165    /// and must be prepared to wait if it returns `false`.
166    fn register_ready_callback<F>(&mut self, _cb: F)
167    where
168        F: Fn() + Send + Sync + 'static,
169    {
170    }
171
172    /// Check if the operator is in a stable state.
173    ///
174    /// This method is invoked as part of checking if the circuit has reached a
175    /// fixed point state, i.e., a state where the outputs of all operators will
176    /// remain constant until the end of the current clock epoch
177    /// (see [`Circuit::fixedpoint`](`crate::circuit::Circuit::fixedpoint`)).
178    ///
179    /// It returns `true` if the operator's output is guaranteed to remain
180    /// constant (i.e., all future outputs will be equal to the last output) as
181    /// long as its inputs remain constant.
182    ///
183    /// The exact semantics depends on the value of the `scope` argument, which
184    /// identifies the circuit whose fixed point state is being checked.
185    /// Scope 0 is the local circuit.  The method is invoked with `scope=0`
186    /// at the end of a clock cycle, and should return `true` if, assuming that
187    /// it will see inputs identical to the last input during all future clock
188    /// cycles in the current clock epoch, it will keep producing the same
189    /// outputs.
190    ///
191    /// Scope 1 represents the parent of the local circuit.  The method is
192    /// invoked with `scope=1` at the end of a clock _epoch_, and should
193    /// return `true` if, assuming that it will see a sequence of inputs
194    /// (aka the input stream) identical to the last epoch during all future
195    /// epochs, it will keep producing the same output streams.
196    ///
197    /// Scope 2 represents the grandparent of the local circuit.  The method is
198    /// invoked with `scope=2` at the end of the parent clock _epoch_, and
199    /// checks that the operator's output will remain stable wrt to the
200    /// nested input stream (i.e., stream of streams).
201    ///
202    /// And so on.
203    ///
204    /// The check must be precise. False positives (returning `true` when the
205    /// output may change in the future) may lead to early termination before
206    /// the circuit has reached a fixed point (and hence incorrect output).
207    /// False negatives (returning `false` in a stable state) is only acceptable
208    /// for a finite number of clock cycles and will otherwise prevent the
209    /// fixedpoint computation from converging.
210    ///
211    /// # Warning
212    ///
213    /// Two operators currently violate this requirement:
214    /// [`Z1`](`crate::operator::Z1`) and
215    /// [`Z1Nested`](`crate::operator::Z1Nested`). The latter will get phased
216    /// out soon.  The former is work-in-progress. It can be safely used inside
217    /// nested circuits when carrying changes to collections across iterations
218    /// of the fixed point computation, but not as part of an integrator circuit
219    /// ([`Stream::integrate`](`crate::circuit::Stream::integrate`)).
220    fn fixedpoint(&self, scope: Scope) -> bool;
221
222    /// Instructs the operator to checkpoint its state to persistent storage in
223    /// directory `base`. Any files that the operator creates should have
224    /// `persistent_id` in their names to keep them unique.
225    ///
226    /// The operator shouldn't commit the state to stable storage; rather, it
227    /// should append the files to be committed to `files` for later commit.
228    ///
229    /// For most operators this method is a no-op.
230    ///
231    /// Fails if the operator is stateful, i.e., expects a checkpoint, by
232    /// `persistent_id` is `None`
233    #[allow(unused_variables)]
234    fn checkpoint(
235        &mut self,
236        base: &StoragePath,
237        persistent_id: Option<&str>,
238        files: &mut Vec<Arc<dyn FileCommitter>>,
239    ) -> Result<(), Error> {
240        Ok(())
241    }
242
243    /// Instruct the operator to restore its state from persistent storage in
244    /// directory `base`, using `persistent_id` to find its files.
245    ///
246    /// For most operators this method is a no-op.
247    #[allow(unused_variables)]
248    fn restore(&mut self, base: &StoragePath, persistent_id: Option<&str>) -> Result<(), Error> {
249        Ok(())
250    }
251
252    /// Clear the operator's state.
253    fn clear_state(&mut self) -> Result<(), Error> {
254        Ok(())
255    }
256
257    /// Start replaying the operator's state to the replay stream.
258    ///
259    /// Only defined for operators that support replay.
260    fn start_replay(&mut self) -> Result<(), Error> {
261        panic!("start_replay() is not implemented for this operator")
262    }
263
264    /// Check if the operator has finished replaying its state.
265    ///
266    /// Only defined for operators that support replay.
267    fn is_replay_complete(&self) -> bool {
268        panic!("is_replay_complete() is not implemented for this operator")
269    }
270
271    /// Cleanup any state needed for replay and prepare the operator for normal operation.
272    ///
273    /// Only defined for operators that support replay.
274    fn end_replay(&mut self) -> Result<(), Error> {
275        panic!("end_replay() is not implemented for this operator")
276    }
277
278    /// Notify the operator about start of a transaction.
279    ///
280    /// The operator can initialize any state needed for the transaction.
281    fn start_transaction(&mut self) {}
282
283    /// Notifies the operator that all of its predecessors have produced
284    /// all outputs for the current transaction.
285    ///
286    /// Operators that wait for all inputs to arrive before producing
287    /// outputs (e.g., join, aggregate, etc.) can use this notification to
288    /// start processing inputs the next time `eval` is invoked.
289    fn flush(&mut self) {}
290
291    /// Invoked after `flush` after each `eval` call to check if all outputs
292    /// have been produced.  Because it is invoked only after calling `eval`,
293    /// every operator must produce at least one output.
294    ///
295    /// Once this method returns `true`, its downstream operators can be flushed.
296    fn is_flush_complete(&self) -> bool {
297        true
298    }
299
300    /// Returns the current progress of the operator in processing the current transaction.
301    ///
302    /// Returns a best-effort estimate of the amount of work done by the operator
303    /// toward processing inputs accumulated before `flush` was called.
304    ///
305    /// Can return `None` if the operator is not in flush mode (i.e., between
306    /// `flush` was called and `is_flush_complete` returns `true`).
307    fn flush_progress(&self) -> Option<Position> {
308        None
309    }
310
311    /// Start compaction of the operator's state.
312    ///
313    /// Only defined for operators that support compaction. No-op for all other operators.
314    fn start_compaction(&mut self) {}
315}
316
317/// A source operator that injects data from the outside world or from the
318/// parent circuit into the local circuit.  Consumes no input streams and emits
319/// a single output stream.
320pub trait SourceOperator<O>: Operator {
321    /// Yield the next value.
322    async fn eval(&mut self) -> O;
323}
324
325/// A sink operator consumes an input stream, but does not produce an output
326/// stream.  Such operators are used to send results of the computation
327/// performed by the circuit to the outside world.
328pub trait SinkOperator<I>: Operator {
329    /// Consume input by reference.
330    async fn eval(&mut self, input: &I);
331
332    /// Consume input by value.
333    async fn eval_owned(&mut self, input: I) {
334        self.eval(&input).await
335    }
336
337    /// Ownership preference on the operator's input stream
338    /// (see [`OwnershipPreference`]).
339    fn input_preference(&self) -> OwnershipPreference {
340        OwnershipPreference::INDIFFERENT
341    }
342}
343
344/// A sink operator that consumes two input streams, but does not produce
345/// an output stream.  Such operators are used to send results of the
346/// computation performed by the circuit to the outside world.
347pub trait BinarySinkOperator<I1, I2>: Operator
348where
349    I1: Clone,
350    I2: Clone,
351{
352    /// Consume inputs.
353    ///
354    /// The operator must be prepared to handle any combination of
355    /// owned and borrowed inputs.
356    async fn eval<'a>(&mut self, lhs: Cow<'a, I1>, rhs: Cow<'a, I2>);
357
358    /// Ownership preference on the operator's input streams
359    /// (see [`OwnershipPreference`]).
360    fn input_preference(&self) -> (OwnershipPreference, OwnershipPreference) {
361        (
362            OwnershipPreference::INDIFFERENT,
363            OwnershipPreference::INDIFFERENT,
364        )
365    }
366}
367
368/// A sink operator that consumes three input streams, but does not produce
369/// an output stream.  Such operators are used to send results of the
370/// computation performed by the circuit to the outside world.
371pub trait TernarySinkOperator<I1, I2, I3>: Operator
372where
373    I1: Clone,
374    I2: Clone,
375    I3: Clone,
376{
377    /// Consume inputs.
378    ///
379    /// The operator must be prepared to handle any combination of
380    /// owned and borrowed inputs.
381    async fn eval<'a>(&mut self, input1: Cow<'a, I1>, input2: Cow<'a, I2>, input3: Cow<'a, I3>);
382
383    /// Ownership preference on the operator's input streams
384    /// (see [`OwnershipPreference`]).
385    fn input_preference(
386        &self,
387    ) -> (
388        OwnershipPreference,
389        OwnershipPreference,
390        OwnershipPreference,
391    ) {
392        (
393            OwnershipPreference::INDIFFERENT,
394            OwnershipPreference::INDIFFERENT,
395            OwnershipPreference::INDIFFERENT,
396        )
397    }
398}
399
400/// A unary operator that consumes a stream of inputs of type `I`
401/// and produces a stream of outputs of type `O`.
402pub trait UnaryOperator<I, O>: Operator {
403    /// Consume input by reference.
404    async fn eval(&mut self, input: &I) -> O;
405
406    /// Consume input by value.
407    async fn eval_owned(&mut self, input: I) -> O {
408        self.eval(&input).await
409    }
410
411    /// Ownership preference on the operator's input stream
412    /// (see [`OwnershipPreference`]).
413    fn input_preference(&self) -> OwnershipPreference {
414        OwnershipPreference::INDIFFERENT
415    }
416}
417
418/// A binary operator consumes two input streams carrying values
419/// of types `I1` and `I2` and produces a stream of outputs of type `O`.
420pub trait BinaryOperator<I1, I2, O>: Operator {
421    /// Consume input by reference.
422    async fn eval(&mut self, lhs: &I1, rhs: &I2) -> O;
423
424    /// Consume input by value.
425    async fn eval_owned(&mut self, lhs: I1, rhs: I2) -> O {
426        self.eval(&lhs, &rhs).await
427    }
428
429    /// Consume the first input by value and the second by reference.
430    async fn eval_owned_and_ref(&mut self, lhs: I1, rhs: &I2) -> O {
431        self.eval(&lhs, rhs).await
432    }
433
434    /// Consume the first input by reference and the second by value.
435    async fn eval_ref_and_owned(&mut self, lhs: &I1, rhs: I2) -> O {
436        self.eval(lhs, &rhs).await
437    }
438
439    /// Ownership preference on the operator's input streams
440    /// (see [`OwnershipPreference`]).
441    fn input_preference(&self) -> (OwnershipPreference, OwnershipPreference) {
442        (
443            OwnershipPreference::INDIFFERENT,
444            OwnershipPreference::INDIFFERENT,
445        )
446    }
447}
448
449/// A ternary operator consumes three input streams carrying values
450/// of types `I1`, `I2`, and `I3` and produces a stream of outputs of type `O`.
451pub trait TernaryOperator<I1, I2, I3, O>: Operator
452where
453    I1: Clone,
454    I2: Clone,
455    I3: Clone,
456{
457    /// Consume inputs.
458    ///
459    /// The operator must be prepared to handle any combination of
460    /// owned and borrowed inputs.
461    async fn eval(&mut self, i1: Cow<'_, I1>, i2: Cow<'_, I2>, i3: Cow<'_, I3>) -> O;
462
463    fn input_preference(
464        &self,
465    ) -> (
466        OwnershipPreference,
467        OwnershipPreference,
468        OwnershipPreference,
469    ) {
470        (
471            OwnershipPreference::INDIFFERENT,
472            OwnershipPreference::INDIFFERENT,
473            OwnershipPreference::INDIFFERENT,
474        )
475    }
476}
477
478/// A quaternary operator consumes four input streams carrying values
479/// of types `I1`, `I2`, `I3`, and `I4` and produces a stream of outputs of type
480/// `O`.
481pub trait QuaternaryOperator<I1, I2, I3, I4, O>: Operator
482where
483    I1: Clone,
484    I2: Clone,
485    I3: Clone,
486    I4: Clone,
487{
488    /// Consume inputs.
489    ///
490    /// The operator must be prepared to handle any combination of
491    /// owned and borrowed inputs.
492    async fn eval(
493        &mut self,
494        i1: Cow<'_, I1>,
495        i2: Cow<'_, I2>,
496        i3: Cow<'_, I3>,
497        i4: Cow<'_, I4>,
498    ) -> O;
499
500    fn input_preference(
501        &self,
502    ) -> (
503        OwnershipPreference,
504        OwnershipPreference,
505        OwnershipPreference,
506        OwnershipPreference,
507    ) {
508        (
509            OwnershipPreference::INDIFFERENT,
510            OwnershipPreference::INDIFFERENT,
511            OwnershipPreference::INDIFFERENT,
512            OwnershipPreference::INDIFFERENT,
513        )
514    }
515}
516
517/// An operator that consumes any number of streams carrying values
518/// of type `I` and produces a stream of outputs of type `O`.
519pub trait NaryOperator<I, O>: Operator
520where
521    I: Clone + 'static,
522{
523    /// Consume inputs.
524    ///
525    /// The operator must be prepared to handle any combination of
526    /// owned and borrowed inputs.
527    async fn eval<'a, Iter>(&'a mut self, inputs: Iter) -> O
528    where
529        Iter: Iterator<Item = Cow<'a, I>>;
530
531    /// Ownership preference on the operator's input streams
532    /// (see [`OwnershipPreference`]).
533    fn input_preference(&self) -> OwnershipPreference {
534        OwnershipPreference::INDIFFERENT
535    }
536}
537
538/// A "strict operator" is one whose output only depends on inputs from previous
539/// timestamps and hence can be produced before consuming new inputs.  This way
540/// a strict operator can be used as part of a feedback loop where its output is
541/// needed before input for the current timestamp is available.
542///
543/// The only strict operators that DBSP makes available are [Z1] and its variant
544/// [Z1Nested].
545///
546/// [Z1]: crate::operator::Z1
547/// [Z1Nested]: crate::operator::Z1Nested
548/// [Z1Trace]: crate::operator::dynamic::trace::Z1Trace
549pub trait StrictOperator<O>: Operator {
550    /// Returns the output value computed based on data consumed by the operator
551    /// during previous timestamps.  This method is invoked **before**
552    /// `eval_strict()` has been invoked for the current timestamp.  It can
553    /// be invoked **at most once** for each timestamp,
554    /// as the implementation may mutate or destroy the operator's internal
555    /// state (for example [Z1](`crate::operator::Z1`) returns its inner
556    /// value, leaving the operator empty).
557    fn get_output(&mut self) -> O;
558
559    fn get_final_output(&mut self) -> O;
560}
561
562/// A strict unary operator that consumes a stream of inputs of type `I`
563/// by reference and produces a stream of outputs of type `O`.
564pub trait StrictUnaryOperator<I, O>: StrictOperator<O> {
565    /// Feed input for the current timestamp to the operator by reference.  The
566    /// output will be consumed via
567    /// [`get_output`](`StrictOperator::get_output`) during the
568    /// next timestamp.
569    async fn eval_strict(&mut self, input: &I);
570
571    /// Feed input for the current timestamp to the operator by value.  The
572    /// output will be consumed via
573    /// [`get_output`](`StrictOperator::get_output`) during the
574    /// next timestamp.
575    async fn eval_strict_owned(&mut self, input: I) {
576        self.eval_strict(&input).await
577    }
578
579    /// Ownership preference on the operator's input stream
580    /// (see [`OwnershipPreference`]).
581    fn input_preference(&self) -> OwnershipPreference {
582        OwnershipPreference::INDIFFERENT
583    }
584
585    /// Flush the input half of the strict operator.
586    ///
587    /// The strict operator appears in the circuit twice: as a source
588    /// operator that outputs the delayed value at the start of the step,
589    /// and as a sink operator that consumes the new value.
590    ///
591    /// These operators are flushed separately. The `Operator::flush` and
592    /// `Operator::is_flush_complete` methods are invoked on the output half.
593    ///
594    /// The `flush_input` and `is_flush_input_complete` methods are invoked
595    /// on the input half.
596    fn flush_input(&mut self);
597
598    /// See [`StrictUnaryOperator::flush_input`] for more details.
599    fn is_flush_input_complete(&self) -> bool;
600}
601
602/// An import operator makes a stream from the parent circuit
603/// available inside a subcircuit.
604///
605/// Import operators are the only kind of operator that span
606/// two clock domains: an import operator reads a single
607/// value from the parent stream per parent clock tick and produces
608/// a stream of outputs in the nested circuit, one for each nested
609/// clock tick.
610///
611/// See [`Delta0`](`crate::operator::Delta0`) for a concrete example
612/// of an import operator.
613pub trait ImportOperator<I, O>: Operator {
614    /// Consumes a value from the parent stream by reference.
615    ///
616    /// Either `import` or [`Self::import_owned`] is invoked once per
617    /// nested clock epoch, right after `clock_start(0)`.
618    async fn import(&mut self, val: &I);
619
620    /// Consumes a value from the parent stream by value.
621    async fn import_owned(&mut self, val: I);
622
623    /// Invoked once per nested clock cycle to write a value to
624    /// the output stream.
625    async fn eval(&mut self) -> O;
626
627    /// Ownership preference on the operator's input stream
628    /// (see [`OwnershipPreference`]).
629    fn input_preference(&self) -> OwnershipPreference {
630        OwnershipPreference::INDIFFERENT
631    }
632}
633
634/// The name of an operator, primarily for use in CPU profiles.
635///
636/// An operator doesn't initially know its global node ID, but the global node
637/// ID is useful for debugging and profiling.  This type allows the name to
638/// initially omit the ID but adds it when it becomes available.
639pub struct OperatorName(ArcSwap<String>);
640
641impl OperatorName {
642    /// Creates a new `OperatorName` that doesn't initially know the global node
643    /// ID, since an operator doesn't know this until [Operator::init] is
644    /// called.
645    ///
646    /// If the name is fetched, with [OperatorName::get], before
647    /// [OperatorName::init], then it will just returned as simply `(base)`.
648    pub fn new(base: &str) -> Self {
649        Self(ArcSwap::new(Arc::new(format!("({base})"))))
650    }
651
652    /// Updates the name to include `global_id`.  This should normally be called
653    /// once, from the implementation of [Operator::init].  If it is called more
654    /// than once then later calls are ignored.
655    ///
656    /// After this function is called, the name will have the format `base
657    /// nodeid` where `nodeid` is in the format used by the circuit profiler so
658    /// that it's easy to find by searching.
659    pub fn init(&self, global_id: &GlobalNodeId) {
660        let s = self.0.load();
661        if let Some(rest) = s.strip_prefix('(')
662            && let Some(base) = rest.strip_suffix(')')
663        {
664            self.0
665                .store(Arc::new(format!("{base} {}", global_id.node_identifier())));
666        }
667    }
668
669    /// Returns a copy of the operator name string.
670    pub fn get(&self) -> Arc<String> {
671        self.0.load_full()
672    }
673}
674
675impl Display for OperatorName {
676    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677        f.write_str(&self.0.load())
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use crate::circuit::{GlobalNodeId, NodeId, operator_traits::OperatorName};
684
685    #[test]
686    fn operator_name() {
687        let name = OperatorName::new("Name");
688        assert_eq!(name.to_string(), "(Name)");
689        name.init(&GlobalNodeId::from_path(&[
690            NodeId::new(1),
691            NodeId::new(2),
692            NodeId::new(3),
693        ]));
694        assert_eq!(name.to_string(), "Name nn1_n2_n3");
695        name.init(&GlobalNodeId::from_path(&[
696            NodeId::new(4),
697            NodeId::new(5),
698            NodeId::new(6),
699        ]));
700        assert_eq!(name.to_string(), "Name nn1_n2_n3");
701    }
702}