From 6b217547d1f72572a9c6318b4cce7b246c59d180 Mon Sep 17 00:00:00 2001 From: jdonszelmann Date: Sat, 25 May 2024 16:51:44 +0200 Subject: [PATCH] redo of sg visualization code --- scopegraphs/Cargo.toml | 3 +- scopegraphs/examples/records.rs | 63 +++++++-- scopegraphs/src/lib.rs | 4 +- scopegraphs/src/render.rs | 116 ---------------- scopegraphs/src/render/mod.rs | 208 +++++++++++++++++++++++++++++ scopegraphs/src/render/traverse.rs | 41 ++++++ 6 files changed, 306 insertions(+), 129 deletions(-) delete mode 100644 scopegraphs/src/render.rs create mode 100644 scopegraphs/src/render/mod.rs create mode 100644 scopegraphs/src/render/traverse.rs diff --git a/scopegraphs/Cargo.toml b/scopegraphs/Cargo.toml index 8a3dbe4..96ea91f 100644 --- a/scopegraphs/Cargo.toml +++ b/scopegraphs/Cargo.toml @@ -17,7 +17,6 @@ rust-version = "1.75" futures = { version = "0.3.30", default-features = false, features = ["std"] } bumpalo = "3.14.0" -dot = { version = "0.1.4", optional = true } scopegraphs-prust-lib = { version = "0.1.0" } log = "0.4.20" @@ -34,7 +33,7 @@ winnow = "0.6.8" [features] default = ["dot", "dynamic-regex"] -dot = ["scopegraphs-regular-expressions/dot", "scopegraphs-macros/dot", "dep:dot"] +dot = ["scopegraphs-regular-expressions/dot", "scopegraphs-macros/dot"] dynamic-regex = ["scopegraphs-regular-expressions/dynamic"] documentation = [] diff --git a/scopegraphs/examples/records.rs b/scopegraphs/examples/records.rs index 226bf38..7b1e1fe 100644 --- a/scopegraphs/examples/records.rs +++ b/scopegraphs/examples/records.rs @@ -3,15 +3,14 @@ use crate::resolve::{resolve_lexical_ref, resolve_member_ref, resolve_record_ref use async_recursion::async_recursion; use futures::future::{join, join_all}; use scopegraphs::completeness::FutureCompleteness; -use scopegraphs::RenderScopeData; +use scopegraphs::render::{EdgeStyle, EdgeTo, RenderScopeData, RenderScopeLabel, RenderSettings}; use scopegraphs::{Scope, ScopeGraph, Storage}; use scopegraphs_macros::Label; use smol::channel::{bounded, Sender}; use smol::LocalExecutor; use std::cell::RefCell; use std::error::Error; -use std::fmt::{Debug, Formatter}; -use std::fs::File; +use std::fmt::{Debug, Display, Formatter}; use std::future::Future; use std::rc::Rc; @@ -22,6 +21,17 @@ enum SgLabel { Lexical, } +impl RenderScopeLabel for SgLabel { + fn render(&self) -> String { + match self { + SgLabel::TypeDefinition => "typ", + SgLabel::Definition => "def", + SgLabel::Lexical => "lex", + } + .to_string() + } +} + #[derive(Debug, Default, Hash, Eq, PartialEq, Clone)] enum SgData { VarDecl { @@ -38,13 +48,38 @@ enum SgData { } impl RenderScopeData for SgData { - fn render(&self) -> Option { + fn render_node(&self) -> Option { + match self { + SgData::VarDecl { name, .. } | SgData::TypeDecl { name, .. } => Some(name.to_string()), + SgData::Nothing => None, + } + } + + fn render_node_label(&self) -> Option { match self { - SgData::VarDecl { name, ty } => Some(format!("var {name}: {ty:?}")), - SgData::TypeDecl { name, scope } => Some(format!("record {name} -> {scope:?}")), + SgData::VarDecl { ty, .. } => { + if matches!(ty, PartialType::Variable(_)) { + None + } else { + Some(ty.to_string()) + } + } + SgData::TypeDecl { .. } => None, SgData::Nothing => None, } } + + fn extra_edges(&self) -> Vec { + if let SgData::TypeDecl { scope, .. } = self { + vec![EdgeTo { + to: *scope, + edge_style: EdgeStyle {}, + label_text: "fields".to_string(), + }] + } else { + Vec::new() + } + } } impl SgData { @@ -80,6 +115,16 @@ enum PartialType { Int, } +impl Display for PartialType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PartialType::Variable(v) => write!(f, "#{}", v.0), + PartialType::Record { name, .. } => write!(f, "record {name}"), + PartialType::Int => write!(f, "int"), + } + } +} + #[derive(Default)] pub struct UnionFind { /// Records the parent of each type variable. @@ -601,7 +646,10 @@ fn typecheck(ast: &Program) -> Option { }); tc.sg - .render(&mut File::create("sg.dot").unwrap(), "sg") + .render_to( + "sg.dot", + RenderSettings::default().with_name("example with records"), + ) .unwrap(); println!("{:?}", tc.uf.borrow()); @@ -808,7 +856,6 @@ main = letrec a = new A {x: 4, b: b}; b = new B {x: 3, a: a}; in a.b.a.x; - ", ) .map_err(|i| i.to_string())?; diff --git a/scopegraphs/src/lib.rs b/scopegraphs/src/lib.rs index 214e5cb..c02b795 100644 --- a/scopegraphs/src/lib.rs +++ b/scopegraphs/src/lib.rs @@ -23,9 +23,7 @@ mod label; pub use label::Label; #[cfg(feature = "dot")] -mod render; -#[cfg(feature = "dot")] -pub use render::RenderScopeData; +pub mod render; pub use scopegraphs_regular_expressions::*; diff --git a/scopegraphs/src/render.rs b/scopegraphs/src/render.rs deleted file mode 100644 index 5191420..0000000 --- a/scopegraphs/src/render.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::completeness::Completeness; -use crate::{Scope, ScopeGraph}; -use dot::LabelText::LabelStr; -use dot::{Edges, Id, LabelText, Nodes}; -use std::borrow::Cow; -use std::fmt::Debug; -use std::fs::File; -use std::io; -use std::io::Write; -use std::path::Path; - -/// Modifies how a scope is rendered based on user-defined scope data. -pub trait RenderScopeData { - /// Renders a scope (or probably rather, the data in a scope). - /// Can return None if there's no data to render. - fn render(&self) -> Option; - - /// Returns whether this scope is a definition of some variable - /// - /// Defaults to whether the outcome of [`render`](RenderScopeData::render) is Some, - /// because often non-definition scopes have no data associated with them. - fn definition(&self) -> bool { - self.render().is_some() - } -} - -impl> - ScopeGraph<'_, LABEL, DATA, CMPL> -{ - /// Visualize the entire scope graph as a graph, by emitting a graphviz dot file. - /// - /// Note: you can also visualize a [single regular expression this way](crate::Automaton::render) - pub fn render(&self, output: &mut W, name: &str) -> io::Result<()> { - dot::render(&(self, name), output) - } - - /// [`render`](ScopeGraph::render) directly to a file. - pub fn render_to(&self, path: impl AsRef) -> io::Result<()> { - let path = path.as_ref(); - let mut w = File::create(path)?; - let name = path - .file_stem() - .expect("path must have filename for File::create to work") - .to_string_lossy(); - self.render(&mut w, &name) - } -} - -impl<'a, LABEL: Clone + Debug, DATA: Clone + RenderScopeData, CMPL: Completeness> - dot::Labeller<'a, Scope, (Scope, LABEL, Scope)> for (&ScopeGraph<'_, LABEL, DATA, CMPL>, &str) -{ - fn graph_id(&'a self) -> Id<'a> { - Id::new(self.1).unwrap() - } - - fn node_id(&'a self, n: &Scope) -> Id<'a> { - Id::new(format!("s{}", n.0)).unwrap() - } - - fn node_shape(&'a self, node: &Scope) -> Option> { - let data = self.0.get_data(*node); - if data.definition() { - Some(LabelStr(Cow::Borrowed("box"))) - } else { - Some(LabelStr(Cow::Borrowed("circle"))) - } - } - fn node_label(&self, n: &Scope) -> LabelText { - let data = self.0.get_data(*n); - LabelText::label(data.render().unwrap_or_else(|| format!("scope {}", n.0))) - } - - fn edge_label(&self, edge: &(Scope, LABEL, Scope)) -> LabelText { - LabelText::label(format!("{:?}", edge.1)) - } -} - -impl<'a, LABEL: Clone, DATA: Clone, CMPL> dot::GraphWalk<'a, Scope, (Scope, LABEL, Scope)> - for (&ScopeGraph<'_, LABEL, DATA, CMPL>, &str) -{ - fn nodes(&'a self) -> Nodes<'a, Scope> { - (0..self.0.inner_scope_graph.data.borrow().len()) - .map(Scope) - .collect() - } - - fn edges(&'a self) -> Edges<'a, (Scope, LABEL, Scope)> { - self.0 - .inner_scope_graph - .edges - .borrow() - .iter() - .enumerate() - .flat_map(|(scope, edges)| { - (*edges) - .borrow() - .iter() - .flat_map(|(lbl, edges_with_lbl)| { - edges_with_lbl - .iter() - .map(|edge| (Scope(scope), lbl.clone(), *edge)) - .collect::>() - }) - .collect::>() - }) - .collect() - } - - fn source(&'a self, edge: &(Scope, LABEL, Scope)) -> Scope { - edge.0 - } - - fn target(&'a self, edge: &(Scope, LABEL, Scope)) -> Scope { - edge.2 - } -} diff --git a/scopegraphs/src/render/mod.rs b/scopegraphs/src/render/mod.rs new file mode 100644 index 0000000..b21da67 --- /dev/null +++ b/scopegraphs/src/render/mod.rs @@ -0,0 +1,208 @@ +//! Render scope graphs to graphviz `.dot` files. +//! +//! Generally, use `sg.render_to(filename, Settings::default()` for the most basic rendering. + +use crate::completeness::Completeness; +use crate::{Scope, ScopeGraph}; +use std::fs::File; +use std::io; +use std::io::Write; +use std::path::Path; + +mod traverse; + +/// Global settings related to rendering scope graphs. +pub struct RenderSettings { + /// Whether to display label text next to edges + pub show_edge_labels: bool, + /// The title which should be displayed above the graph. + /// + /// Defaults to the filename given to [`ScopeGraph::render_to`]. + pub title: Option, +} + +impl RenderSettings { + /// Sets the name of the scope graph + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.title = Some(name.as_ref().to_string()); + self + } +} + +impl Default for RenderSettings { + fn default() -> Self { + Self { + show_edge_labels: true, + title: None, + } + } +} + +/// The style of an edge (TODO) +pub struct EdgeStyle {} + +/// An edge from one scope to another +pub struct Edge { + /// the source scope, where the edge starts + pub from: Scope, + /// A description of the destination, as well as all actual edge information. + pub to: EdgeTo, +} + +/// Information about the destination and style of an edge from one scope to another. +pub struct EdgeTo { + /// the destination + pub to: Scope, + /// what style does this edge get? + pub edge_style: EdgeStyle, + /// What label is displayed next to the edge + /// + /// Note: this label is hidden if you disable `show_label_text` in [`RenderSettings`]. + pub label_text: String, +} + +/// Modifies how an edge label is rendered. +pub trait RenderScopeLabel { + /// Render a single label + fn render(&self) -> String; +} + +/// Modifies how a scope is rendered based on user-defined scope data. +pub trait RenderScopeData { + /// Renders a scope (or probably rather, the data in a scope) + /// in the scope graph. This will be shown in the middle of the node. + /// + /// Can return None if there's no data to render. + fn render_node(&self) -> Option { + None + } + + /// Renders a scope (or probably rather, the data in a scope) + /// in the scope graph. This will be shown next to the node. + /// + /// Can return None if there's no data to render. + fn render_node_label(&self) -> Option { + None + } + + /// Returns any extra edge your scope might want to render. + /// + /// If a scope's data contains a reference to another scope, + /// this is like a hidden edge you might want to draw. + fn extra_edges(&self) -> Vec { + Vec::new() + } + + /// Returns whether this scope is a definition of some variable + /// + /// Defaults to whether the outcome of [`render`](RenderScopeData::render_node_label) is Some, + /// because often non-definition scopes have no data associated with them. + fn definition(&self) -> bool { + self.render_node().is_some() + } +} + +fn scope_to_node_name(s: Scope) -> String { + format!("scope_{}", s.0) +} + +fn escape_text(inp: &str) -> String { + inp.replace('"', "\\\"") +} + +impl< + LABEL: Clone + RenderScopeLabel, + DATA: RenderScopeData + Clone, + CMPL: Completeness, + > ScopeGraph<'_, LABEL, DATA, CMPL> +{ + /// Visualize the entire scope graph as a graph, by emitting a graphviz dot file. + /// + /// Note: you can also visualize a [single regular expression this way](crate::Automaton::render) + pub fn render(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> { + let (mut edges, nodes) = traverse::traverse(self); + + writeln!(output, "digraph {{")?; + + // color scheme + writeln!( + output, + r#"node [colorscheme="ylgnbu6",width="0.1",height="0.1",margin="0.01",xlp="b"]"# + )?; + + if let Some(ref i) = settings.title { + // title + writeln!(output, r#"labelloc="t";"#)?; + writeln!(output, r#"label="{}";"#, escape_text(i))?; + } + + // straight edges + writeln!(output, r#"splines=false;"#)?; + + // nodes + for (scope, data) in nodes { + edges.extend( + data.extra_edges() + .into_iter() + .map(|to| Edge { from: scope, to }), + ); + let name = scope_to_node_name(scope); + + let mut attrs = Vec::new(); + + if data.definition() { + attrs.push(r#"[shape="square"]"#.to_string()) + } else { + attrs.push(r#"[shape="circle"]"#.to_string()) + }; + let label = escape_text(&data.render_node().unwrap_or_else(|| scope.0.to_string())); + attrs.push(format!(r#"[label="{label}"]"#)); + + if let Some(label) = data.render_node_label() { + attrs.push(format!(r#"[xlabel="{}"]"#, escape_text(&label))) + } + + attrs.push(r#"[penwidth="2.0"]"#.to_string()); + + writeln!(output, r#"{name} {}"#, attrs.join(""))? + } + + // edges + for edge in edges { + let from = scope_to_node_name(edge.from); + let to = scope_to_node_name(edge.to.to); + let label = edge.to.label_text; + + if settings.show_edge_labels { + writeln!(output, "{from} -> {to} [label={label}]")? + } else { + writeln!(output, "{from} -> {to}")? + } + } + + writeln!(output, "}}")?; + + Ok(()) + } + + /// [`render`](ScopeGraph::render) directly to a file. + pub fn render_to( + &self, + path: impl AsRef, + mut settings: RenderSettings, + ) -> io::Result<()> { + let path = path.as_ref(); + let mut w = File::create(path)?; + + if settings.title.is_none() { + settings.title = Some( + path.file_stem() + .expect("path must have filename for File::create to work") + .to_string_lossy() + .to_string(), + ); + } + + self.render(&mut w, settings) + } +} diff --git a/scopegraphs/src/render/traverse.rs b/scopegraphs/src/render/traverse.rs new file mode 100644 index 0000000..93b1742 --- /dev/null +++ b/scopegraphs/src/render/traverse.rs @@ -0,0 +1,41 @@ +use crate::completeness::Completeness; +use crate::render::{Edge, EdgeStyle, EdgeTo, RenderScopeLabel}; +use crate::{Scope, ScopeGraph}; + +pub fn traverse<'sg, LABEL: RenderScopeLabel, DATA, CMPL: Completeness>( + sg: &'sg ScopeGraph<'_, LABEL, DATA, CMPL>, +) -> (Vec, Vec<(Scope, &'sg DATA)>) { + let edges: Vec<_> = sg + .inner_scope_graph + .edges + .borrow() + .iter() + .enumerate() + .flat_map(|(scope, edges)| { + (*edges) + .borrow() + .iter() + .flat_map(|(lbl, edges_with_lbl)| { + edges_with_lbl + .iter() + .map(|edge| Edge { + from: Scope(scope), + to: EdgeTo { + to: *edge, + edge_style: EdgeStyle {}, + label_text: lbl.render(), + }, + }) + .collect::>() + }) + .collect::>() + }) + .collect(); + + let nodes = (0..sg.inner_scope_graph.data.borrow().len()) + .map(Scope) + .map(|i| (i, sg.get_data(i))) + .collect(); + + (edges, nodes) +}