Skip to content

Commit cce7cc4

Browse files
committed
feat: add api to output for graphviz and mermaid
1 parent 2251fbd commit cce7cc4

File tree

3 files changed

+260
-2
lines changed

3 files changed

+260
-2
lines changed

simpcli/src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ fn usage(process_name: &str) {
2828
eprintln!("Usage:");
2929
eprintln!(" {} assemble <filename>", process_name);
3030
eprintln!(" {} disassemble <base64>", process_name);
31+
eprintln!(" {} graph <base64>", process_name);
3132
eprintln!(" {} relabel <base64>", process_name);
3233
eprintln!();
3334
eprintln!("For commands which take an optional expression, the default value is \"main\".");
@@ -43,6 +44,7 @@ fn invalid_usage(process_name: &str) -> Result<(), String> {
4344
enum Command {
4445
Assemble,
4546
Disassemble,
47+
Graph,
4648
Relabel,
4749
Help,
4850
}
@@ -53,6 +55,7 @@ impl FromStr for Command {
5355
match s {
5456
"assemble" => Ok(Command::Assemble),
5557
"disassemble" => Ok(Command::Disassemble),
58+
"graphviz" | "dot" | "graph" => Ok(Command::Graph),
5659
"relabel" => Ok(Command::Relabel),
5760
"help" => Ok(Command::Help),
5861
x => Err(format!("unknown command {}", x)),
@@ -65,6 +68,7 @@ impl Command {
6568
match *self {
6669
Command::Assemble => false,
6770
Command::Disassemble => false,
71+
Command::Graph => false,
6872
Command::Relabel => false,
6973
Command::Help => false,
7074
}
@@ -155,6 +159,14 @@ fn main() -> Result<(), String> {
155159
let prog = Forest::<DefaultJet>::from_program(commit);
156160
println!("{}", prog.string_serialize());
157161
}
162+
Command::Graph => {
163+
let v = simplicity::base64::Engine::decode(&STANDARD, first_arg.as_bytes())
164+
.map_err(|e| format!("failed to parse base64: {}", e))?;
165+
let iter = BitIter::from(v.into_iter());
166+
let commit = CommitNode::<DefaultJet>::decode(iter)
167+
.map_err(|e| format!("failed to decode program: {}", e))?;
168+
println!("{}", commit.display_as_dot());
169+
}
158170
Command::Relabel => {
159171
let prog = parse_file(&first_arg)?;
160172
println!("{}", prog.string_serialize());

src/node/display.rs

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::fmt;
22
use std::sync::OnceLock;
33

4-
use crate::dag::{Dag, DagLike, InternalSharing, MaxSharing, NoSharing};
4+
use crate::dag::{
5+
Dag, DagLike, InternalSharing, MaxSharing, NoSharing, PostOrderIterItem, SharingTracker,
6+
};
57
use crate::encode;
68
use crate::node::{Inner, Marker, Node};
79
use crate::BitWriter;
@@ -197,6 +199,166 @@ where
197199
}
198200
}
199201

202+
/// The output format for [`DisplayAsGraph`].
203+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204+
pub 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)]
213+
pub 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.
229+
pub 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+
237+
impl<'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+
336+
impl<'a, M: Marker> fmt::Display for DisplayAsGraph<'a, M>
337+
where
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+
345+
fn 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+
200362
#[cfg(test)]
201363
mod tests {
202364
use crate::human_encoding::Forest;
@@ -241,4 +403,54 @@ mod tests {
241403
program.display_expr().to_string()
242404
)
243405
}
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 {
422+
ordering=\"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+
}
244456
}

src/node/mod.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub use commit::{Commit, CommitData, CommitNode};
8585
pub use construct::{Construct, ConstructData, ConstructNode};
8686
pub use convert::{Converter, Hide, SimpleFinalizer};
8787
pub use disconnect::{Disconnectable, NoDisconnect};
88-
pub use display::{Display, DisplayExpr};
88+
pub use display::{Display, DisplayAsGraph, DisplayExpr, GraphFormat, SharingLevel};
8989
pub use hiding::Hiding;
9090
pub use inner::Inner;
9191
pub use redeem::{Redeem, RedeemData, RedeemNode};
@@ -723,6 +723,40 @@ impl<N: Marker> Node<N> {
723723
DisplayExpr::from(self)
724724
}
725725

726+
/// Display the Simplicity expression as a graph in the given format and sharing level.
727+
///
728+
/// This is the general form of [`display_as_dot`](Node::display_as_dot) and
729+
/// [`display_as_mermaid`](Node::display_as_mermaid). Use those convenience methods for
730+
/// the common case of DOT or Mermaid output with no sharing.
731+
///
732+
/// The `format` field of the returned [`DisplayAsGraph`] can be changed after construction,
733+
/// and the [`fmt::Display`] impl will use whatever `format` and `sharing` are set at that
734+
/// point. See also [`DisplayAsGraph::to_dot_string`] and [`DisplayAsGraph::to_mermaid_string`]
735+
/// to render to a specific format regardless of the stored `format` field.
736+
pub fn display_as_graph(
737+
&self,
738+
format: GraphFormat,
739+
sharing_level: SharingLevel,
740+
) -> DisplayAsGraph<'_, N> {
741+
DisplayAsGraph::new(self, format, sharing_level)
742+
}
743+
744+
/// Display the Simplicity expression as a Graphviz DOT graph.
745+
///
746+
/// The DOT output can be rendered with `dot -Tsvg` or similar tools.
747+
/// Shared nodes appear once in the graph with multiple incoming edges.
748+
pub fn display_as_dot(&self) -> DisplayAsGraph<'_, N> {
749+
DisplayAsGraph::new(self, GraphFormat::Dot, SharingLevel::None)
750+
}
751+
752+
/// Display the Simplicity expression as a Mermaid diagram.
753+
///
754+
/// The Mermaid output can be rendered in Markdown or the Mermaid live editor.
755+
/// Shared nodes appear once in the diagram with multiple incoming edges.
756+
pub fn display_as_mermaid(&self) -> DisplayAsGraph<'_, N> {
757+
DisplayAsGraph::new(self, GraphFormat::Mermaid, SharingLevel::None)
758+
}
759+
726760
/// Encode a Simplicity expression to bits without any witness data.
727761
pub fn encode_without_witness<W: io::Write>(&self, prog: W) -> io::Result<usize> {
728762
let mut w = BitWriter::new(prog);

0 commit comments

Comments
 (0)