Skip to main content

simplicity/node/
display.rs

1use std::fmt;
2use std::sync::OnceLock;
3
4use crate::dag::{
5    Dag, DagLike, InternalSharing, MaxSharing, NoSharing, PostOrderIterItem, SharingTracker,
6};
7use crate::encode;
8use crate::node::{Inner, Marker, Node};
9use crate::BitWriter;
10
11/// Convenience structure for displaying a Simplictiy expression with its
12/// witness.
13pub struct Display<'n, M: Marker> {
14    node: &'n Node<M>,
15    #[cfg_attr(not(feature = "base64"), allow(dead_code))]
16    prog_bytes: OnceLock<Vec<u8>>,
17    wit_bytes: OnceLock<Vec<u8>>,
18}
19
20impl<'n, M: Marker> From<&'n Node<M>> for Display<'n, M> {
21    fn from(node: &'n Node<M>) -> Self {
22        // Because of Rust's lack of specialization we cannot cache the witness data
23        // until we're in a function which is gated on `M: Marker<Witness = Value>`.
24        // So we use `OnceLock` for that.
25        //
26        // While we're at it, use `OnceLock` for the program bytes, since maybe the
27        // user doesn't want the program data (or can't use it due to lack of base64).
28        Self {
29            node,
30            prog_bytes: OnceLock::new(),
31            wit_bytes: OnceLock::new(),
32        }
33    }
34}
35
36impl<'n, M: Marker> Display<'n, M> {
37    /// Display the program in base64.
38    #[cfg(feature = "base64")]
39    pub fn program(&self) -> impl fmt::Display + '_ {
40        use crate::base64::display::Base64Display;
41        use crate::base64::engine::general_purpose;
42
43        let prog_bytes = self
44            .prog_bytes
45            .get_or_init(|| self.node.to_vec_without_witness());
46        Base64Display::new(prog_bytes, &general_purpose::STANDARD)
47    }
48}
49
50impl<'n, M: Marker<Witness = crate::Value>> Display<'n, M> {
51    /// Display the witness data in hex.
52    pub fn witness(&self) -> impl fmt::Display + '_ {
53        use crate::hex::DisplayHex;
54
55        let wit_bytes = self.wit_bytes.get_or_init(|| {
56            let mut wit_v = vec![];
57            let mut witness = BitWriter::new(&mut wit_v);
58            let sharing_iter = self.node.post_order_iter::<MaxSharing<M>>();
59
60            encode::encode_witness(sharing_iter.into_witnesses(), &mut witness)
61                .expect("Vec::write is infallible");
62            witness.flush_all().expect("Vec::write is infallible");
63
64            wit_v
65        });
66
67        wit_bytes.as_hex()
68    }
69}
70
71/// Display a Simplicity expression as a linear string.
72///
73/// The linear string has no sharing and may be **exponentially larger**
74/// than the underlying shared expression.
75///
76/// There are some basic transformations to increase readability:
77///
78/// ## Infix notation
79///
80/// `pair s t` → `s & t`
81///
82/// `comp s t` → `s; t`
83///
84/// ## Booleans
85///
86/// `injl unit` → `false`
87///
88/// `injr unit` → `true`
89///
90/// ## Selectors
91///
92/// `take drop iden` → `OIH`
93///
94/// Sequences of `take` and `drop` that end in `iden` are transformed as follows:
95///
96/// `take` → `O` (looks like zero)
97///
98/// `drop` → `I` (looks like one)
99///
100/// `iden` → `H`
101pub struct DisplayExpr<'a, M: Marker>(&'a Node<M>);
102
103impl<'a, M: Marker> From<&'a Node<M>> for DisplayExpr<'a, M> {
104    fn from(node: &'a Node<M>) -> Self {
105        Self(node)
106    }
107}
108
109impl<'a, M: Marker> fmt::Display for DisplayExpr<'a, M>
110where
111    &'a Node<M>: DagLike,
112{
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        for data in self.0.verbose_pre_order_iter::<NoSharing>(None) {
115            match data.n_children_yielded {
116                1 => match data.node.inner() {
117                    Inner::Comp(..) => f.write_str("; ")?,
118                    Inner::Pair(..) => f.write_str(" & ")?,
119                    Inner::Case(..) => f.write_str(") (")?,
120                    x => debug_assert!(matches!(x.as_dag(), Dag::Unary(..))),
121                },
122                2 => {
123                    debug_assert!(matches!(data.node.inner().as_dag(), Dag::Binary(..)));
124                    if data.parent.is_some() {
125                        f.write_str(")")?;
126                    } else {
127                        // Omit parentheses for root node
128                    }
129                }
130                n => {
131                    debug_assert!(n == 0, "Combinators are nullary, unary or binary");
132                    match data.node.inner() {
133                        Inner::Iden
134                            if matches!(
135                                data.parent.map(Node::inner),
136                                Some(Inner::Take(..) | Inner::Drop(..))
137                            ) =>
138                        {
139                            f.write_str("H")?
140                        }
141                        Inner::Take(child) if is_take_drop_iden(child) => f.write_str("O")?,
142                        Inner::Drop(child) if is_take_drop_iden(child) => f.write_str("I")?,
143                        Inner::Iden => f.write_str("iden")?,
144                        Inner::Take(..) => f.write_str("take ")?,
145                        Inner::Drop(..) => f.write_str("drop ")?,
146                        Inner::Unit
147                            if matches!(
148                                data.parent.map(Node::inner),
149                                Some(Inner::InjL(..) | Inner::InjR(..))
150                            ) => {} // skip unit inside false | true
151                        Inner::InjL(child) if matches!(child.inner(), Inner::Unit) => {
152                            f.write_str("false")?
153                        }
154                        Inner::InjR(child) if matches!(child.inner(), Inner::Unit) => {
155                            f.write_str("true")?
156                        }
157                        Inner::Unit => f.write_str("unit")?,
158                        Inner::InjL(..) => f.write_str("injl ")?,
159                        Inner::InjR(..) => f.write_str("injr ")?,
160                        Inner::Comp(..) | Inner::Pair(..) => {} // display comp and pair as infix
161                        Inner::Case(..) => f.write_str("case ")?,
162                        Inner::AssertL(..) => f.write_str("assertl ")?,
163                        Inner::AssertR(..) => f.write_str("assertr ")?,
164                        Inner::Disconnect(..) => f.write_str("disconnect ")?,
165                        Inner::Witness(..) => f.write_str("witness ")?,
166                        Inner::Fail(..) => f.write_str("fail")?,
167                        Inner::Jet(jet) => write!(f, "jet_{jet} ")?,
168                        Inner::Word(value) => write!(f, "const {value} ")?,
169                    }
170
171                    match data.node.inner().as_dag() {
172                        Dag::Binary(..) if data.parent.is_some() => f.write_str("(")?,
173                        _ => {} // Omit parentheses for root node
174                    }
175                }
176            };
177        }
178
179        Ok(())
180    }
181}
182
183fn is_take_drop_iden<M: Marker>(node: &Node<M>) -> bool {
184    for node in node.pre_order_iter::<InternalSharing>() {
185        match node.inner() {
186            Inner::Take(..) | Inner::Drop(..) | Inner::Iden => {}
187            _ => return false,
188        }
189    }
190    true
191}
192
193impl<'a, M: Marker> fmt::Debug for DisplayExpr<'a, M>
194where
195    &'a Node<M>: DagLike,
196{
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        fmt::Display::fmt(self, f)
199    }
200}
201
202/// The output format for [`DisplayAsGraph`].
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204pub enum GraphFormat {
205    /// Graphviz DOT format, renderable with `dot -Tsvg` or similar tools.
206    Dot,
207    /// Mermaid diagram format, renderable in Markdown or the Mermaid live editor.
208    Mermaid,
209}
210
211/// The node-sharing level for [`DisplayAsGraph`].
212#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum SharingLevel {
214    /// No sharing: every use of a node is visited separately (may be exponentially large).
215    None,
216    /// Internal sharing: nodes shared within the expression are visited once.
217    Internal,
218    /// Maximum sharing: maximize sharing across the entire expression.
219    Max,
220}
221
222/// Display a Simplicity expression as a graph in a chosen format.
223///
224/// Construct via [`Node::display_as_dot`], [`Node::display_as_mermaid`], or
225/// [`DisplayAsGraph::new`]. The [`fmt::Display`] impl renders using the stored
226/// `format` and `sharing` fields; [`to_dot_string`](DisplayAsGraph::to_dot_string)
227/// and [`to_mermaid_string`](DisplayAsGraph::to_mermaid_string) always render in
228/// the named format using the stored sharing level.
229pub struct DisplayAsGraph<'a, M: Marker> {
230    node: &'a Node<M>,
231    /// Output format (DOT or Mermaid).
232    pub format: GraphFormat,
233    /// Node-sharing level used when rendering.
234    pub sharing: SharingLevel,
235}
236
237impl<'a, M: Marker> DisplayAsGraph<'a, M> {
238    /// Create a new `DisplayAsGraph` with the given format and sharing level.
239    pub fn new(node: &'a Node<M>, format: GraphFormat, sharing: SharingLevel) -> Self {
240        Self {
241            node,
242            format,
243            sharing,
244        }
245    }
246
247    /// Render as a Graphviz DOT string using the stored sharing level.
248    pub fn to_dot_string(&self) -> String
249    where
250        &'a Node<M>: DagLike,
251    {
252        let mut result = String::new();
253        match self.render(GraphFormat::Dot, &mut result) {
254            Ok(_) => result,
255            Err(e) => format!("Could not display as string: {}", e),
256        }
257    }
258
259    /// Render as a Mermaid string using the stored sharing level.
260    pub fn to_mermaid_string(&self) -> String
261    where
262        &'a Node<M>: DagLike,
263    {
264        let mut result = String::new();
265        match self.render(GraphFormat::Mermaid, &mut result) {
266            Ok(_) => result,
267            Err(e) => format!("Could not display as string: {}", e),
268        }
269    }
270
271    fn render<W: fmt::Write>(&self, graph_format: GraphFormat, w: &mut W) -> fmt::Result
272    where
273        &'a Node<M>: DagLike,
274    {
275        match self.sharing {
276            SharingLevel::None => self.render_with::<NoSharing, _>(graph_format, w),
277            SharingLevel::Internal => self.render_with::<InternalSharing, _>(graph_format, w),
278            SharingLevel::Max => self.render_with::<MaxSharing<M>, _>(graph_format, w),
279        }
280    }
281
282    fn render_with<S, W>(&self, graph_format: GraphFormat, w: &mut W) -> fmt::Result
283    where
284        S: SharingTracker<&'a Node<M>> + Default,
285        W: fmt::Write,
286    {
287        let node_label = |data: &PostOrderIterItem<&Node<M>>| -> String {
288            match data.node.inner() {
289                Inner::Witness(_) => format!("witness({})", data.index),
290                Inner::Word(word) => format!("word({})", shorten(word.to_string(), 12)),
291                _ => data.node.inner().to_string(),
292            }
293        };
294
295        match graph_format {
296            GraphFormat::Dot => {
297                writeln!(w, "digraph G {{")?;
298                writeln!(w, "ordering=\"out\";")?;
299                for data in self.node.post_order_iter::<S>() {
300                    writeln!(w, "  node{}[label=\"{}\"];", data.index, node_label(&data))?;
301                    if let Some(left) = data.left_index {
302                        writeln!(w, "  node{}->node{};", data.index, left)?;
303                    }
304                    if let Some(right) = data.right_index {
305                        writeln!(w, "  node{}->node{};", data.index, right)?;
306                    }
307                }
308                writeln!(w, "}}")?;
309            }
310            GraphFormat::Mermaid => {
311                writeln!(w, "flowchart TD")?;
312                for data in self.node.post_order_iter::<S>() {
313                    match data.node.inner() {
314                        Inner::Case(..) => {
315                            writeln!(w, "  node{}{{\"{}\"}}", data.index, node_label(&data))?;
316                        }
317                        _ => {
318                            writeln!(w, "  node{}[\"{}\"]", data.index, node_label(&data))?;
319                        }
320                    }
321
322                    if let Some(left) = data.left_index {
323                        writeln!(w, "  node{} --> node{}", data.index, left)?;
324                    }
325                    if let Some(right) = data.right_index {
326                        writeln!(w, "  node{} --> node{}", data.index, right)?;
327                    }
328                }
329            }
330        }
331
332        Ok(())
333    }
334}
335
336impl<'a, M: Marker> fmt::Display for DisplayAsGraph<'a, M>
337where
338    &'a Node<M>: DagLike,
339{
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        self.render(self.format, f)
342    }
343}
344
345fn shorten<S: AsRef<str>>(s: S, max_len: usize) -> String {
346    let s = s.as_ref();
347    let chars: Vec<char> = s.chars().collect();
348    if chars.len() <= max_len {
349        s.to_string()
350    } else {
351        let dots = "...";
352        let available = max_len.saturating_sub(dots.len());
353        let start_len = available.div_ceil(2); // Slightly favor the start
354        let end_len = available / 2;
355
356        let start: String = chars[..start_len].iter().collect();
357        let end: String = chars[chars.len() - end_len..].iter().collect();
358        format!("{}{}{}", start, dots, end)
359    }
360}
361
362#[cfg(all(test, feature = "human_encoding"))]
363mod tests {
364    use crate::human_encoding::Forest;
365    use crate::jet::Core;
366    use crate::types;
367    use crate::RedeemNode;
368    use std::collections::HashMap;
369    use std::sync::Arc;
370
371    fn parse_program(s: &str) -> Arc<RedeemNode> {
372        types::Context::with_context(|ctx| {
373            let empty_witness = HashMap::new();
374            Forest::parse::<Core>(s)
375                .unwrap()
376                .to_witness_node(&ctx, &empty_witness)
377                .unwrap()
378                .finalize_unpruned()
379                .unwrap()
380        })
381    }
382
383    #[test]
384    fn display_boolean() {
385        let s = "
386            false := injl unit
387            true := injr unit
388            main := comp pair false true unit";
389        let program = parse_program(s);
390        assert_eq!("(false & true); unit", program.display_expr().to_string())
391    }
392
393    #[test]
394    fn display_oih() {
395        let s = "
396            oih := take drop iden
397            input := pair (pair unit unit) unit
398            output := unit
399            main := comp input (comp (pair oih (take unit)) output)";
400        let program = parse_program(s);
401        assert_eq!(
402            "((unit & unit) & unit); ((OIH & take unit); unit)",
403            program.display_expr().to_string()
404        )
405    }
406
407    #[test]
408    fn display_as_dot() {
409        let s = "
410            oih := take drop iden
411            input := pair (pair unit unit) unit
412            output := unit
413            main := comp input (comp (pair oih (take unit)) output)";
414        let program = parse_program(s);
415        let str = program
416            .display_as_dot()
417            .to_string()
418            .replace(" ", "")
419            .replace("\n", "");
420        let expected = "
421        digraph G {
422ordering=\"out\";
423  node0[label=\"unit\"];
424  node1[label=\"unit\"];
425  node2[label=\"pair\"];
426  node2->node0;
427  node2->node1;
428  node3[label=\"unit\"];
429  node4[label=\"pair\"];
430  node4->node2;
431  node4->node3;
432  node5[label=\"iden\"];
433  node6[label=\"drop\"];
434  node6->node5;
435  node7[label=\"take\"];
436  node7->node6;
437  node8[label=\"unit\"];
438  node9[label=\"take\"];
439  node9->node8;
440  node10[label=\"pair\"];
441  node10->node7;
442  node10->node9;
443  node11[label=\"unit\"];
444  node12[label=\"comp\"];
445  node12->node10;
446  node12->node11;
447  node13[label=\"comp\"];
448  node13->node4;
449  node13->node12;
450}"
451        .replace(" ", "")
452        .replace("\n", "");
453
454        assert_eq!(str, expected);
455    }
456}