|
1 | 1 | use std::fmt; |
2 | 2 | use std::sync::OnceLock; |
3 | 3 |
|
4 | | -use crate::dag::{Dag, DagLike, InternalSharing, MaxSharing, NoSharing}; |
| 4 | +use crate::dag::{ |
| 5 | + Dag, DagLike, InternalSharing, MaxSharing, NoSharing, PostOrderIterItem, SharingTracker, |
| 6 | +}; |
5 | 7 | use crate::encode; |
6 | 8 | use crate::node::{Inner, Marker, Node}; |
7 | 9 | use crate::BitWriter; |
@@ -197,6 +199,166 @@ where |
197 | 199 | } |
198 | 200 | } |
199 | 201 |
|
| 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 | + |
200 | 362 | #[cfg(test)] |
201 | 363 | mod tests { |
202 | 364 | use crate::human_encoding::Forest; |
@@ -241,4 +403,54 @@ mod tests { |
241 | 403 | program.display_expr().to_string() |
242 | 404 | ) |
243 | 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 { |
| 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 | + } |
244 | 456 | } |
0 commit comments