From a2dc7203d1b9136cca552b84c8424343bb428dd5 Mon Sep 17 00:00:00 2001 From: jdonszelmann Date: Wed, 5 Jun 2024 16:29:31 +0200 Subject: [PATCH] render paths & resolved paths on scopegraphs --- scopegraphs-render-docs/src/attrs.rs | 19 ++-- scopegraphs/src/concepts/mod.rs | 154 ++++++++++++++++++++++++++- scopegraphs/src/render/mod.rs | 63 ++++++++--- scopegraphs/src/resolve/mod.rs | 41 +++++++ 4 files changed, 255 insertions(+), 22 deletions(-) diff --git a/scopegraphs-render-docs/src/attrs.rs b/scopegraphs-render-docs/src/attrs.rs index 90b76a7..1e5de65 100644 --- a/scopegraphs-render-docs/src/attrs.rs +++ b/scopegraphs-render-docs/src/attrs.rs @@ -96,13 +96,20 @@ impl quote::ToTokens for Attrs { .map(Attr::expect_diagram_entry_text) .collect::>(); - tokens.extend(quote! {#[doc = "```rust"]}); - for i in &diagram { - tokens.extend(quote! { - #[doc = #i] - }); + if !diagram + .iter() + .filter(|i| !i.trim().is_empty()) + .all(|i| i.trim().starts_with('#')) + && !diagram.is_empty() + { + tokens.extend(quote! {#[doc = "```rust"]}); + for i in &diagram { + tokens.extend(quote! { + #[doc = #i] + }); + } + tokens.extend(quote! {#[doc = "```"]}); } - tokens.extend(quote! {#[doc = "```"]}); match generate_diagram_rustdoc(&diagram) { Ok(i) => { diff --git a/scopegraphs/src/concepts/mod.rs b/scopegraphs/src/concepts/mod.rs index 5dba75f..4ce11a1 100644 --- a/scopegraphs/src/concepts/mod.rs +++ b/scopegraphs/src/concepts/mod.rs @@ -479,7 +479,7 @@ pub mod scope_data {} /// Note, it's common to create new scopes for each variable definition like this. /// /// A rough approximation of a program which would have such a scope structure would be: -/// ```rust, no_run +/// ```ignore /// // in the global scope /// let bar = 3; /// fn foo() { @@ -513,7 +513,7 @@ pub mod scope_data {} /// /// Let's go for the simple case first. Let's say we now write the following example: /// -/// ```rust, no_run +/// ```ignore /// // in the global scope /// let bar = 3; /// fn foo() { @@ -528,10 +528,83 @@ pub mod scope_data {} /// So first, we look in the current scope: `foo`'s scope. We immediately find a `Definition` /// edge which brings us to a variable definition with the name `baz`. So we're done! /// +/// ```rust +/// # use scopegraphs::*; +/// # use completeness::{UncheckedCompleteness}; +/// # use resolve::{DataWellformedness, Resolve, ResolvedPath}; +/// # use render::{RenderSettings, RenderScopeData, RenderScopeLabel}; +/// # +/// # #[derive(Label, Hash, PartialEq, Eq, Debug, Clone, Copy)] +/// # enum Lbl { +/// # Lexical, +/// # Definition, +/// # } +/// # +/// # #[derive(Hash, PartialEq, Eq, Debug, Default, Clone)] +/// # enum Data<'a> { +/// # #[default] +/// # NoData, +/// # Variable { +/// # name: &'a str, +/// # }, +/// # } +/// # +/// # impl RenderScopeData for Data<'_> { +/// # fn render_node(&self) -> Option { +/// # match self { +/// # Self::Variable {..} => Some(format!("{self:?}")), +/// # _ => None, +/// # } +/// # } +/// # +/// # fn definition(&self) -> bool { +/// # matches!(self, Self::Variable {..}) +/// # } +/// # } +/// # +/// # impl RenderScopeLabel for Lbl { +/// # fn render(&self) -> String { +/// # match self { +/// # Self::Lexical => "lexical", +/// # Self::Definition => "definition", +/// # }.to_string() +/// # } +/// # } +/// # +/// # let storage = Storage::new(); +/// # let sg: ScopeGraph = +/// # unsafe { ScopeGraph::raw(&storage) }; +/// # +/// # let global = sg.add_scope_default(); +/// # let fn_foo = sg.add_scope_default(); +/// # +/// # // create a scope in which the variable `bar` is defined +/// # let declares_a_global = sg.add_scope(Data::Variable {name: "bar"}); +/// # +/// # // create another scope in which the variable `bar` is defined inside foo +/// # let declares_a_local_in_foo = sg.add_scope(Data::Variable {name: "baz"}); +/// # +/// # // Add some edges +/// # sg.add_edge(fn_foo, Lbl::Lexical, global); +/// # +/// # sg.add_edge(global, Lbl::Definition, declares_a_global); +/// # sg.add_edge(fn_foo, Lbl::Definition, declares_a_local_in_foo); +/// # +/// # let res = sg.query() +/// # .with_path_wellformedness(query_regex!(Lbl: Lexical* Definition)) +/// # .with_data_wellformedness(|a: &Data| matches!(a, Data::Variable {name: "baz"})) +/// # .resolve(fn_foo); +/// # +/// # sg.render_to("output.mmd", RenderSettings { +/// # path: Some(res.get_only_item().unwrap()), +/// # ..Default::default() +/// # }).unwrap() +/// ``` +/// /// ## Example 2: in the enclosing (global) scope /// Alright, now for a slightly more complicated example: /// -/// ```rust, no_run +/// ```ignore /// // in the global scope /// let bar = 3; /// fn foo() { @@ -546,12 +619,85 @@ pub mod scope_data {} /// So, we can choose to instead traverse the `Lexical` edge to look in the global scope. /// Now we *can* find a definition of `bar` (using a `Definition` edge), so we're done. /// +/// ```rust +/// # use scopegraphs::*; +/// # use completeness::{UncheckedCompleteness}; +/// # use resolve::{DataWellformedness, Resolve, ResolvedPath}; +/// # use render::{RenderSettings, RenderScopeData, RenderScopeLabel}; +/// # +/// # #[derive(Label, Hash, PartialEq, Eq, Debug, Clone, Copy)] +/// # enum Lbl { +/// # Lexical, +/// # Definition, +/// # } +/// # +/// # #[derive(Hash, PartialEq, Eq, Debug, Default, Clone)] +/// # enum Data<'a> { +/// # #[default] +/// # NoData, +/// # Variable { +/// # name: &'a str, +/// # }, +/// # } +/// # +/// # impl RenderScopeData for Data<'_> { +/// # fn render_node(&self) -> Option { +/// # match self { +/// # Self::Variable {..} => Some(format!("{self:?}")), +/// # _ => None, +/// # } +/// # } +/// # +/// # fn definition(&self) -> bool { +/// # matches!(self, Self::Variable {..}) +/// # } +/// # } +/// # +/// # impl RenderScopeLabel for Lbl { +/// # fn render(&self) -> String { +/// # match self { +/// # Self::Lexical => "lexical", +/// # Self::Definition => "definition", +/// # }.to_string() +/// # } +/// # } +/// # +/// # let storage = Storage::new(); +/// # let sg: ScopeGraph = +/// # unsafe { ScopeGraph::raw(&storage) }; +/// # +/// # let global = sg.add_scope_default(); +/// # let fn_foo = sg.add_scope_default(); +/// # +/// # // create a scope in which the variable `bar` is defined +/// # let declares_a_global = sg.add_scope(Data::Variable {name: "bar"}); +/// # +/// # // create another scope in which the variable `bar` is defined inside foo +/// # let declares_a_local_in_foo = sg.add_scope(Data::Variable {name: "baz"}); +/// # +/// # // Add some edges +/// # sg.add_edge(fn_foo, Lbl::Lexical, global); +/// # +/// # sg.add_edge(global, Lbl::Definition, declares_a_global); +/// # sg.add_edge(fn_foo, Lbl::Definition, declares_a_local_in_foo); +/// # +/// # let res = sg.query() +/// # .with_path_wellformedness(query_regex!(Lbl: Lexical* Definition)) +/// # .with_data_wellformedness(|a: &Data| matches!(a, Data::Variable {name: "bar"})) +/// # .resolve(fn_foo); +/// # +/// # sg.render_to("output.mmd", RenderSettings { +/// # path: Some(res.get_only_item().unwrap()), +/// # ..Default::default() +/// # }).unwrap() +/// ``` +/// /// ## Example 3: when name resolution fails /// /// Finally, let's look at an example in which name resolution should obviously fail, /// and discuss why it does, using the scope graph we constructed. /// -/// ```rust, no_run +/// ```ignore /// // in the global scope /// let bar = 3; /// fn foo() { diff --git a/scopegraphs/src/render/mod.rs b/scopegraphs/src/render/mod.rs index e92d469..d637bbf 100644 --- a/scopegraphs/src/render/mod.rs +++ b/scopegraphs/src/render/mod.rs @@ -3,6 +3,7 @@ //! Generally, use `sg.render_to(filename, Settings::default()` for the most basic rendering. use crate::completeness::Completeness; +use crate::resolve::ResolvedPath; use crate::{Scope, ScopeGraph}; use std::fs::File; use std::io; @@ -22,7 +23,7 @@ pub enum Target { } /// Global settings related to rendering scope graphs. -pub struct RenderSettings { +pub struct RenderSettings<'sg, LABEL, DATA> { /// Whether to display label text next to edges pub show_edge_labels: bool, /// The title which should be displayed above the graph. @@ -31,9 +32,11 @@ pub struct RenderSettings { pub title: Option, /// The output format to use for the visualization. pub target: Target, + /// A resolved path that should also be rendered. Useful for debugging queries. + pub path: Option>, } -impl RenderSettings { +impl<'sg, LABEL, DATA> RenderSettings<'sg, LABEL, DATA> { /// 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()); @@ -41,12 +44,13 @@ impl RenderSettings { } } -impl Default for RenderSettings { +impl<'sg, LABEL, DATA> Default for RenderSettings<'sg, LABEL, DATA> { fn default() -> Self { Self { show_edge_labels: true, title: None, target: Default::default(), + path: None, } } } @@ -136,14 +140,22 @@ impl< /// 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<()> { + pub fn render( + &self, + output: &mut W, + settings: RenderSettings<'_, LABEL, DATA>, + ) -> io::Result<()> { match settings.target { Target::Dot => self.render_dot(output, settings), Target::Mermaid => self.render_mermaid(output, settings), } } - fn render_mermaid(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> { + fn render_mermaid( + &self, + output: &mut W, + settings: RenderSettings<'_, LABEL, DATA>, + ) -> io::Result<()> { let (mut edges, nodes) = traverse::traverse(self); if let Some(ref i) = settings.title { @@ -193,22 +205,49 @@ impl< } // edges - for edge in edges { - let from = scope_to_node_name(edge.from); - let to = scope_to_node_name(edge.to.to); + for (idx, edge) in edges.iter().enumerate() { + let (from, to) = (edge.from, edge.to.to); + + let from_str = scope_to_node_name(from); + let to_str = scope_to_node_name(to); let label = escape_text_mermaid(&edge.to.label_text); if settings.show_edge_labels { - writeln!(output, r#"{from} ==>|"{label}"| {to}"#)? + writeln!(output, r#"{from_str} ==>|"{label}"| {to_str}"#)? } else { - writeln!(output, " {from} ==> {to}")? + writeln!(output, " {from_str} ==> {to_str}")? + } + + if let Some(ref i) = settings.path { + let mut prev = None; + let mut part_of_path = false; + for curr in i.scopes() { + if let Some(ref mut prev) = prev { + if (to, from) == (*prev, curr) { + part_of_path = true; + break; + } + + *prev = curr; + } else { + prev = Some(curr); + } + } + + if part_of_path { + writeln!(output, "linkStyle {idx} stroke: red")?; + } } } Ok(()) } - fn render_dot(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> { + fn render_dot( + &self, + output: &mut W, + settings: RenderSettings<'_, LABEL, DATA>, + ) -> io::Result<()> { let (mut edges, nodes) = traverse::traverse(self); writeln!(output, "digraph {{")?; @@ -278,7 +317,7 @@ impl< pub fn render_to( &self, path: impl AsRef, - mut settings: RenderSettings, + mut settings: RenderSettings<'_, LABEL, DATA>, ) -> io::Result<()> { let path = path.as_ref(); let mut w = File::create(path)?; diff --git a/scopegraphs/src/resolve/mod.rs b/scopegraphs/src/resolve/mod.rs index 6d8f3d3..afaf19e 100644 --- a/scopegraphs/src/resolve/mod.rs +++ b/scopegraphs/src/resolve/mod.rs @@ -74,6 +74,40 @@ pub struct Path