simplicity/node/
display.rs

1use std::fmt;
2use std::sync::OnceLock;
3
4use crate::dag::{Dag, DagLike, InternalSharing, MaxSharing, NoSharing};
5use crate::encode;
6use crate::node::{Inner, Marker, Node};
7use crate::BitWriter;
8
9/// Convenience structure for displaying a Simplictiy expression with its
10/// witness.
11pub struct Display<'n, M: Marker> {
12    node: &'n Node<M>,
13    #[cfg_attr(not(feature = "base64"), allow(dead_code))]
14    prog_bytes: OnceLock<Vec<u8>>,
15    wit_bytes: OnceLock<Vec<u8>>,
16}
17
18impl<'n, M: Marker> From<&'n Node<M>> for Display<'n, M> {
19    fn from(node: &'n Node<M>) -> Self {
20        // Because of Rust's lack of specialization we cannot cache the witness data
21        // until we're in a function which is gated on `M: Marker<Witness = Value>`.
22        // So we use `OnceLock` for that.
23        //
24        // While we're at it, use `OnceLock` for the program bytes, since maybe the
25        // user doesn't want the program data (or can't use it due to lack of base64).
26        Self {
27            node,
28            prog_bytes: OnceLock::new(),
29            wit_bytes: OnceLock::new(),
30        }
31    }
32}
33
34impl<'n, M: Marker> Display<'n, M> {
35    /// Display the program in base64.
36    #[cfg(feature = "base64")]
37    pub fn program(&self) -> impl fmt::Display + '_ {
38        use crate::base64::display::Base64Display;
39        use crate::base64::engine::general_purpose;
40
41        let prog_bytes = self
42            .prog_bytes
43            .get_or_init(|| self.node.to_vec_without_witness());
44        Base64Display::new(prog_bytes, &general_purpose::STANDARD)
45    }
46}
47
48impl<'n, M: Marker<Witness = crate::Value>> Display<'n, M> {
49    /// Display the witness data in hex.
50    pub fn witness(&self) -> impl fmt::Display + '_ {
51        use crate::hex::DisplayHex;
52
53        let wit_bytes = self.wit_bytes.get_or_init(|| {
54            let mut wit_v = vec![];
55            let mut witness = BitWriter::new(&mut wit_v);
56            let sharing_iter = self.node.post_order_iter::<MaxSharing<M>>();
57
58            encode::encode_witness(sharing_iter.into_witnesses(), &mut witness)
59                .expect("Vec::write is infallible");
60            witness.flush_all().expect("Vec::write is infallible");
61
62            wit_v
63        });
64
65        wit_bytes.as_hex()
66    }
67}
68
69/// Display a Simplicity expression as a linear string.
70///
71/// The linear string has no sharing and may be **exponentially larger**
72/// than the underlying shared expression.
73///
74/// There are some basic transformations to increase readability:
75///
76/// ## Infix notation
77///
78/// `pair s t` → `s & t`
79///
80/// `comp s t` → `s; t`
81///
82/// ## Booleans
83///
84/// `injl unit` → `false`
85///
86/// `injr unit` → `true`
87///
88/// ## Selectors
89///
90/// `take drop iden` → `OIH`
91///
92/// Sequences of `take` and `drop` that end in `iden` are transformed as follows:
93///
94/// `take` → `O` (looks like zero)
95///
96/// `drop` → `I` (looks like one)
97///
98/// `iden` → `H`
99pub struct DisplayExpr<'a, M: Marker>(&'a Node<M>);
100
101impl<'a, M: Marker> From<&'a Node<M>> for DisplayExpr<'a, M> {
102    fn from(node: &'a Node<M>) -> Self {
103        Self(node)
104    }
105}
106
107impl<'a, M: Marker> fmt::Display for DisplayExpr<'a, M>
108where
109    &'a Node<M>: DagLike,
110{
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        for data in self.0.verbose_pre_order_iter::<NoSharing>(None) {
113            match data.n_children_yielded {
114                1 => match data.node.inner() {
115                    Inner::Comp(..) => f.write_str("; ")?,
116                    Inner::Pair(..) => f.write_str(" & ")?,
117                    Inner::Case(..) => f.write_str(") (")?,
118                    x => debug_assert!(matches!(x.as_dag(), Dag::Unary(..))),
119                },
120                2 => {
121                    debug_assert!(matches!(data.node.inner().as_dag(), Dag::Binary(..)));
122                    if data.parent.is_some() {
123                        f.write_str(")")?;
124                    } else {
125                        // Omit parentheses for root node
126                    }
127                }
128                n => {
129                    debug_assert!(n == 0, "Combinators are nullary, unary or binary");
130                    match data.node.inner() {
131                        Inner::Iden
132                            if matches!(
133                                data.parent.map(Node::inner),
134                                Some(Inner::Take(..) | Inner::Drop(..))
135                            ) =>
136                        {
137                            f.write_str("H")?
138                        }
139                        Inner::Take(child) if is_take_drop_iden(child) => f.write_str("O")?,
140                        Inner::Drop(child) if is_take_drop_iden(child) => f.write_str("I")?,
141                        Inner::Iden => f.write_str("iden")?,
142                        Inner::Take(..) => f.write_str("take ")?,
143                        Inner::Drop(..) => f.write_str("drop ")?,
144                        Inner::Unit
145                            if matches!(
146                                data.parent.map(Node::inner),
147                                Some(Inner::InjL(..) | Inner::InjR(..))
148                            ) => {} // skip unit inside false | true
149                        Inner::InjL(child) if matches!(child.inner(), Inner::Unit) => {
150                            f.write_str("false")?
151                        }
152                        Inner::InjR(child) if matches!(child.inner(), Inner::Unit) => {
153                            f.write_str("true")?
154                        }
155                        Inner::Unit => f.write_str("unit")?,
156                        Inner::InjL(..) => f.write_str("injl ")?,
157                        Inner::InjR(..) => f.write_str("injr ")?,
158                        Inner::Comp(..) | Inner::Pair(..) => {} // display comp and pair as infix
159                        Inner::Case(..) => f.write_str("case ")?,
160                        Inner::AssertL(..) => f.write_str("assertl ")?,
161                        Inner::AssertR(..) => f.write_str("assertr ")?,
162                        Inner::Disconnect(..) => f.write_str("disconnect ")?,
163                        Inner::Witness(..) => f.write_str("witness ")?,
164                        Inner::Fail(..) => f.write_str("fail")?,
165                        Inner::Jet(jet) => write!(f, "jet_{jet} ")?,
166                        Inner::Word(value) => write!(f, "const {value} ")?,
167                    }
168
169                    match data.node.inner().as_dag() {
170                        Dag::Binary(..) if data.parent.is_some() => f.write_str("(")?,
171                        _ => {} // Omit parentheses for root node
172                    }
173                }
174            };
175        }
176
177        Ok(())
178    }
179}
180
181fn is_take_drop_iden<M: Marker>(node: &Node<M>) -> bool {
182    for node in node.pre_order_iter::<InternalSharing>() {
183        match node.inner() {
184            Inner::Take(..) | Inner::Drop(..) | Inner::Iden => {}
185            _ => return false,
186        }
187    }
188    true
189}
190
191impl<'a, M: Marker> fmt::Debug for DisplayExpr<'a, M>
192where
193    &'a Node<M>: DagLike,
194{
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        fmt::Display::fmt(self, f)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use crate::human_encoding::Forest;
203    use crate::jet::Core;
204    use crate::types;
205    use crate::RedeemNode;
206    use std::collections::HashMap;
207    use std::sync::Arc;
208
209    fn parse_program(s: &str) -> Arc<RedeemNode<Core>> {
210        types::Context::with_context(|ctx| {
211            let empty_witness = HashMap::new();
212            Forest::<Core>::parse(s)
213                .unwrap()
214                .to_witness_node(&ctx, &empty_witness)
215                .unwrap()
216                .finalize_unpruned()
217                .unwrap()
218        })
219    }
220
221    #[test]
222    fn display_boolean() {
223        let s = "
224            false := injl unit
225            true := injr unit
226            main := comp pair false true unit";
227        let program = parse_program(s);
228        assert_eq!("(false & true); unit", program.display_expr().to_string())
229    }
230
231    #[test]
232    fn display_oih() {
233        let s = "
234            oih := take drop iden
235            input := pair (pair unit unit) unit
236            output := unit
237            main := comp input (comp (pair oih (take unit)) output)";
238        let program = parse_program(s);
239        assert_eq!(
240            "((unit & unit) & unit); ((OIH & take unit); unit)",
241            program.display_expr().to_string()
242        )
243    }
244}