Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions simpcli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fn usage(process_name: &str) {
eprintln!("Usage:");
eprintln!(" {} assemble <filename>", process_name);
eprintln!(" {} disassemble <base64>", process_name);
eprintln!(" {} graph <base64>", process_name);
eprintln!(" {} relabel <base64>", process_name);
eprintln!();
eprintln!("For commands which take an optional expression, the default value is \"main\".");
Expand All @@ -43,6 +44,7 @@ fn invalid_usage(process_name: &str) -> Result<(), String> {
enum Command {
Assemble,
Disassemble,
Graph,
Relabel,
Help,
}
Expand All @@ -53,6 +55,7 @@ impl FromStr for Command {
match s {
"assemble" => Ok(Command::Assemble),
"disassemble" => Ok(Command::Disassemble),
"graphviz" | "dot" | "graph" => Ok(Command::Graph),
"relabel" => Ok(Command::Relabel),
"help" => Ok(Command::Help),
x => Err(format!("unknown command {}", x)),
Expand All @@ -65,6 +68,7 @@ impl Command {
match *self {
Command::Assemble => false,
Command::Disassemble => false,
Command::Graph => false,
Command::Relabel => false,
Command::Help => false,
}
Expand Down Expand Up @@ -155,6 +159,14 @@ fn main() -> Result<(), String> {
let prog = Forest::<DefaultJet>::from_program(commit);
println!("{}", prog.string_serialize());
}
Command::Graph => {
let v = simplicity::base64::Engine::decode(&STANDARD, first_arg.as_bytes())
.map_err(|e| format!("failed to parse base64: {}", e))?;
let iter = BitIter::from(v.into_iter());
let commit = CommitNode::<DefaultJet>::decode(iter)
.map_err(|e| format!("failed to decode program: {}", e))?;
println!("{}", commit.display_as_dot());
}
Command::Relabel => {
let prog = parse_file(&first_arg)?;
println!("{}", prog.string_serialize());
Expand Down
214 changes: 213 additions & 1 deletion src/node/display.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::fmt;
use std::sync::OnceLock;

use crate::dag::{Dag, DagLike, InternalSharing, MaxSharing, NoSharing};
use crate::dag::{
Dag, DagLike, InternalSharing, MaxSharing, NoSharing, PostOrderIterItem, SharingTracker,
};
use crate::encode;
use crate::node::{Inner, Marker, Node};
use crate::BitWriter;
Expand Down Expand Up @@ -197,6 +199,166 @@ where
}
}

/// The output format for [`DisplayAsGraph`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GraphFormat {
/// Graphviz DOT format, renderable with `dot -Tsvg` or similar tools.
Dot,
/// Mermaid diagram format, renderable in Markdown or the Mermaid live editor.
Mermaid,
}

/// The node-sharing level for [`DisplayAsGraph`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SharingLevel {
/// No sharing: every use of a node is visited separately (may be exponentially large).
None,
/// Internal sharing: nodes shared within the expression are visited once.
Internal,
/// Maximum sharing: maximize sharing across the entire expression.
Max,
}

/// Display a Simplicity expression as a graph in a chosen format.
///
/// Construct via [`Node::display_as_dot`], [`Node::display_as_mermaid`], or
/// [`DisplayAsGraph::new`]. The [`fmt::Display`] impl renders using the stored
/// `format` and `sharing` fields; [`to_dot_string`](DisplayAsGraph::to_dot_string)
/// and [`to_mermaid_string`](DisplayAsGraph::to_mermaid_string) always render in
/// the named format using the stored sharing level.
pub struct DisplayAsGraph<'a, M: Marker> {
node: &'a Node<M>,
/// Output format (DOT or Mermaid).
pub format: GraphFormat,
/// Node-sharing level used when rendering.
pub sharing: SharingLevel,
}

impl<'a, M: Marker> DisplayAsGraph<'a, M> {
/// Create a new `DisplayAsGraph` with the given format and sharing level.
pub fn new(node: &'a Node<M>, format: GraphFormat, sharing: SharingLevel) -> Self {
Self {
node,
format,
sharing,
}
}

/// Render as a Graphviz DOT string using the stored sharing level.
pub fn to_dot_string(&self) -> String
where
&'a Node<M>: DagLike,
{
let mut result = String::new();
match self.render(GraphFormat::Dot, &mut result) {
Ok(_) => result,
Err(e) => format!("Could not display as string: {}", e),
}
}

/// Render as a Mermaid string using the stored sharing level.
pub fn to_mermaid_string(&self) -> String
where
&'a Node<M>: DagLike,
{
let mut result = String::new();
match self.render(GraphFormat::Mermaid, &mut result) {
Ok(_) => result,
Err(e) => format!("Could not display as string: {}", e),
}
}

fn render<W: fmt::Write>(&self, graph_format: GraphFormat, w: &mut W) -> fmt::Result
where
&'a Node<M>: DagLike,
{
match self.sharing {
SharingLevel::None => self.render_with::<NoSharing, _>(graph_format, w),
SharingLevel::Internal => self.render_with::<InternalSharing, _>(graph_format, w),
SharingLevel::Max => self.render_with::<MaxSharing<M>, _>(graph_format, w),
}
}

fn render_with<S, W>(&self, graph_format: GraphFormat, w: &mut W) -> fmt::Result
where
S: SharingTracker<&'a Node<M>> + Default,
W: fmt::Write,
{
let node_label = |data: &PostOrderIterItem<&Node<M>>| -> String {
match data.node.inner() {
Inner::Witness(_) => format!("witness({})", data.index),
Inner::Word(word) => format!("word({})", shorten(word.to_string(), 12)),
_ => data.node.inner().to_string(),
}
};

match graph_format {
GraphFormat::Dot => {
writeln!(w, "digraph G {{")?;
writeln!(w, "ordering=\"out\";")?;
for data in self.node.post_order_iter::<S>() {
writeln!(w, " node{}[label=\"{}\"];", data.index, node_label(&data))?;
if let Some(left) = data.left_index {
writeln!(w, " node{}->node{};", data.index, left)?;
}
if let Some(right) = data.right_index {
writeln!(w, " node{}->node{};", data.index, right)?;
}
}
writeln!(w, "}}")?;
}
GraphFormat::Mermaid => {
writeln!(w, "flowchart TD")?;
for data in self.node.post_order_iter::<S>() {
match data.node.inner() {
Inner::Case(..) => {
writeln!(w, " node{}{{\"{}\"}}", data.index, node_label(&data))?;
}
_ => {
writeln!(w, " node{}[\"{}\"]", data.index, node_label(&data))?;
}
}

if let Some(left) = data.left_index {
writeln!(w, " node{} --> node{}", data.index, left)?;
}
if let Some(right) = data.right_index {
writeln!(w, " node{} --> node{}", data.index, right)?;
}
}
}
}

Ok(())
}
}

impl<'a, M: Marker> fmt::Display for DisplayAsGraph<'a, M>
where
&'a Node<M>: DagLike,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(self.format, f)
}
}

fn shorten<S: AsRef<str>>(s: S, max_len: usize) -> String {
let s = s.as_ref();
let chars: Vec<char> = s.chars().collect();
if chars.len() <= max_len {
s.to_string()
} else {
let dots = "...";
let available = max_len.saturating_sub(dots.len());
let start_len = available.div_ceil(2); // Slightly favor the start
let end_len = available / 2;

let start: String = chars[..start_len].iter().collect();
let end: String = chars[chars.len() - end_len..].iter().collect();
format!("{}{}{}", start, dots, end)
}
}

#[cfg(test)]
mod tests {
use crate::human_encoding::Forest;
Expand Down Expand Up @@ -241,4 +403,54 @@ mod tests {
program.display_expr().to_string()
)
}

#[test]
fn display_as_dot() {
let s = "
oih := take drop iden
input := pair (pair unit unit) unit
output := unit
main := comp input (comp (pair oih (take unit)) output)";
let program = parse_program(s);
let str = program
.display_as_dot()
.to_string()
.replace(" ", "")
.replace("\n", "");
let expected = "
digraph G {
ordering=\"out\";
node0[label=\"unit\"];
node1[label=\"unit\"];
node2[label=\"pair\"];
node2->node0;
node2->node1;
node3[label=\"unit\"];
node4[label=\"pair\"];
node4->node2;
node4->node3;
node5[label=\"iden\"];
node6[label=\"drop\"];
node6->node5;
node7[label=\"take\"];
node7->node6;
node8[label=\"unit\"];
node9[label=\"take\"];
node9->node8;
node10[label=\"pair\"];
node10->node7;
node10->node9;
node11[label=\"unit\"];
node12[label=\"comp\"];
node12->node10;
node12->node11;
node13[label=\"comp\"];
node13->node4;
node13->node12;
}"
.replace(" ", "")
.replace("\n", "");

assert_eq!(str, expected);
}
}
36 changes: 35 additions & 1 deletion src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub use commit::{Commit, CommitData, CommitNode};
pub use construct::{Construct, ConstructData, ConstructNode};
pub use convert::{Converter, Hide, SimpleFinalizer};
pub use disconnect::{Disconnectable, NoDisconnect};
pub use display::{Display, DisplayExpr};
pub use display::{Display, DisplayAsGraph, DisplayExpr, GraphFormat, SharingLevel};
pub use hiding::Hiding;
pub use inner::Inner;
pub use redeem::{Redeem, RedeemData, RedeemNode};
Expand Down Expand Up @@ -723,6 +723,40 @@ impl<N: Marker> Node<N> {
DisplayExpr::from(self)
}

/// Display the Simplicity expression as a graph in the given format and sharing level.
///
/// This is the general form of [`display_as_dot`](Node::display_as_dot) and
/// [`display_as_mermaid`](Node::display_as_mermaid). Use those convenience methods for
/// the common case of DOT or Mermaid output with no sharing.
///
/// The `format` field of the returned [`DisplayAsGraph`] can be changed after construction,
/// and the [`fmt::Display`] impl will use whatever `format` and `sharing` are set at that
/// point. See also [`DisplayAsGraph::to_dot_string`] and [`DisplayAsGraph::to_mermaid_string`]
/// to render to a specific format regardless of the stored `format` field.
pub fn display_as_graph(
&self,
format: GraphFormat,
sharing_level: SharingLevel,
) -> DisplayAsGraph<'_, N> {
DisplayAsGraph::new(self, format, sharing_level)
}

/// Display the Simplicity expression as a Graphviz DOT graph.
///
/// The DOT output can be rendered with `dot -Tsvg` or similar tools.
/// Shared nodes appear once in the graph with multiple incoming edges.
pub fn display_as_dot(&self) -> DisplayAsGraph<'_, N> {
DisplayAsGraph::new(self, GraphFormat::Dot, SharingLevel::None)
}

/// Display the Simplicity expression as a Mermaid diagram.
///
/// The Mermaid output can be rendered in Markdown or the Mermaid live editor.
/// Shared nodes appear once in the diagram with multiple incoming edges.
pub fn display_as_mermaid(&self) -> DisplayAsGraph<'_, N> {
DisplayAsGraph::new(self, GraphFormat::Mermaid, SharingLevel::None)
}

/// Encode a Simplicity expression to bits without any witness data.
pub fn encode_without_witness<W: io::Write>(&self, prog: W) -> io::Result<usize> {
let mut w = BitWriter::new(prog);
Expand Down
Loading