From b27e43c7253b453eee6647fa12ad8efff9e27ef5 Mon Sep 17 00:00:00 2001 From: jdonszelmann Date: Wed, 5 Jun 2024 14:45:12 +0200 Subject: [PATCH 01/14] document edges --- Justfile | 6 + scopegraphs-render-docs/src/attrs.rs | 32 ++--- scopegraphs/src/concepts/mod.rs | 201 ++++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 19 deletions(-) diff --git a/Justfile b/Justfile index 9c614ca..0554ccc 100644 --- a/Justfile +++ b/Justfile @@ -1,2 +1,8 @@ watch-docs: cargo +nightly watch -s 'cargo +nightly docs-rs -p scopegraphs && browser-sync start --ss target/x86_64-unknown-linux-gnu/doc -s target/x86_64-unknown-linux-gnu/doc --directory --no-open' + +build-docs: + rm -rf /tmp/SG_TARGET* + cargo +nightly docs-rs -p scopegraphs + rm -rf /tmp/SG_TARGET* + diff --git a/scopegraphs-render-docs/src/attrs.rs b/scopegraphs-render-docs/src/attrs.rs index 1d7fdc1..dcdcf9b 100644 --- a/scopegraphs-render-docs/src/attrs.rs +++ b/scopegraphs-render-docs/src/attrs.rs @@ -519,19 +519,19 @@ fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec let mut ctx: Ctx<'_> = Default::default(); let flush_buffer_as_doc_comment = |ctx: &mut Ctx| { - if !ctx.buffer.is_empty() { - ctx.attrs.push(Attr::DocComment( - ident.clone(), - ctx.buffer.drain(..).join(" "), - )); - } + // if !ctx.buffer.is_empty() { + ctx.attrs.push(Attr::DocComment( + ident.clone(), + ctx.buffer.drain(..).join(" "), + )); + // } }; let flush_buffer_as_diagram_entry = |ctx: &mut Ctx| { let s = ctx.buffer.drain(..).join(" "); - if !s.trim().is_empty() { - ctx.attrs.push(Attr::DiagramEntry(ident.clone(), s)); - } + // if !s.trim().is_empty() { + ctx.attrs.push(Attr::DiagramEntry(ident.clone(), s)); + // } }; while let Some(token) = tokens.next() { @@ -563,13 +563,13 @@ fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec } } - if !ctx.buffer.is_empty() { - if loc.is_inside() { - flush_buffer_as_diagram_entry(&mut ctx); - } else { - flush_buffer_as_doc_comment(&mut ctx); - }; - } + // if !ctx.buffer.is_empty() { + if loc.is_inside() { + flush_buffer_as_diagram_entry(&mut ctx); + } else { + flush_buffer_as_doc_comment(&mut ctx); + }; + // } ctx.attrs } diff --git a/scopegraphs/src/concepts/mod.rs b/scopegraphs/src/concepts/mod.rs index 6b03b53..82d403e 100644 --- a/scopegraphs/src/concepts/mod.rs +++ b/scopegraphs/src/concepts/mod.rs @@ -39,6 +39,13 @@ use scopegraphs_render_docs::render_scopegraphs; /// ``` /// The scope contains two names: `a` and `b`. /// +/// Other examples of scopes are +/// * Functions +/// * Any other block using braces like `if` and `loop` +/// * Modules +/// * Structs, or classes in other languages than Rust (they contain fields) +/// * traits (they contain methods) +/// /// In scope graphs, every scope has some kind of associated [scope data](crate::concepts::scope_data), /// and any number of [connections](crate::concepts::edges) to other scopes (0 connections is also fine). /// @@ -256,6 +263,7 @@ use scopegraphs_render_docs::render_scopegraphs; /// /// sg.render_to("output.mmd", RenderSettings::default()).unwrap() /// ``` +/// /// And as you can see, this renders two scopes (0 and 1), not connected by any [edges](crate::concepts::edges) yet. /// /// Now that you know how to make a scope, I recommend you continue reading about [scope data](crate::concepts::scope_data) @@ -354,8 +362,8 @@ pub mod scope {} /// ///
/// -/// Note: if you want to have multiple variable declarations in one scope, you likely do not want to simply make the Data a [`Vec`](std::vec::Vec). -/// Instead, it's common to make declarations their own scope. We'll look at examples of this in the chapter on [edges](crate::concepts::edges) +/// Note: if you want to have multiple variable definitions in one scope, you likely do not want to simply make the Data a [`Vec`](std::vec::Vec). +/// Instead, it's common to make definitions their own scope. We'll look at examples of this in the chapter on [edges](crate::concepts::edges) /// ///
/// @@ -388,7 +396,194 @@ pub mod scope {} pub mod scope_data {} #[render_scopegraphs] -/// Edges connect [`Scopes`](crate::concepts::scope) +/// Edges connect [`Scopes`](crate::concepts::scope) together, based on the rules of the language you're trying to create. +/// By adding edges, you're really starting to use scope graphs. +/// +/// You can think of edges as the path a name resolution can take. Edges are always directed. +/// Let's construct a simple scope graph with some edges, and discuss it: +/// +/// ```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); +/// +/// sg.render_to("output.mmd", RenderSettings::default()).unwrap() +/// ``` +/// +/// Alright, let's analyse the example! +/// +/// In total we define 4 scopes. +/// * A global scope, which in the visualization gets ID 0 +/// * A local scope, which in the visualization gets ID 1 +/// * A scope in which `bar` is defined +/// * A scope in which `baz` is defined +/// +/// 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 +/// // in the global scope +/// let bar = 3; +/// fn foo() { +/// // in foo's scope +/// let baz = 4; +/// } +/// ``` +/// +/// We gave the four scopes various edges between to reflect this program structure +/// +/// * there's an edge between `foo`'s scope and the global scope labelled `Lexical`, +/// which represents the fact that the function `foo` is *in* the global scope, and code *in* +/// foo can access the global scope. +/// * there are two edges labelled `Definition`, to represent that a certain name is defined in that scope. +/// * `bar` is declared +/// * `baz` is declared in fn `foo` +/// +/// As you can see, edges here have *labels*. +/// Labels can be an arbitrary type, in the example it's an enum with several options. +/// Simply put, labels define in _what way_ two scopes are related. `Lexical` and `Definition` are common labels, +/// but you could also imagine a `Imports`, `Inherits` or `TypeDefinition` label being useful in a programming language. +/// These labels can later be used to give direction to [queries](crate::concepts) over the scope graph. +/// +/// # Looking up symbols in a scope graph +/// +/// The point of a scope graph is that after we have one, we can use it to compute name resolution. +/// Although we won't get into [queries](crate::concepts) yet, what would such a lookup look like for the tiny +/// example we've just constructed? +/// +/// ## Example 1: in the local scope +/// +/// Let's go for the simple case first. Let's say we now write the following example: +/// +/// ```rust, no_run +/// // in the global scope +/// let bar = 3; +/// fn foo() { +/// // in foo's scope +/// let baz = 4; +/// +/// println!("{}", baz); +/// } +/// ``` +/// +/// On the last line, we want to look up the definition of `baz` to print it. +/// 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! +/// +/// ## Example 2: in the enclosing (global) scope +/// Alright, now for a slightly more complicated example: +/// +/// ```rust, no_run +/// // in the global scope +/// let bar = 3; +/// fn foo() { +/// // in foo's scope +/// let baz = 4; +/// +/// println!("{}", bar); +/// } +/// ``` +/// +/// Now, we cannot find a definition of `bar` in `foo`'s scope directly. +/// 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. +/// +/// ## 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 +/// // in the global scope +/// let bar = 3; +/// fn foo() { +/// // in foo's scope +/// let baz = 4; +/// } +/// println!("{}", baz); +/// ``` +/// Now, we try to access `baz` from the global scope, +/// but we shouldn't be able to because `baz` is defined inside `foo`'s scope. +/// +/// So what would happen when we performed name resolution here? +/// Well, we would start in the global scope. There's only a definition of `bar` there, +/// so we don't instantly succeed. However, we also cannot look any further. +/// There's no `Lexical` edge from the global scope to `foo`'s scope. +/// +/// Thus, no results are found when looking up `baz` here. +/// +///
+/// +/// I've heard of people who are confused by the direction of the edges. They would say something like: +/// In the example, `foo` is _inside of_ the global scope, so why is there an edge _from_ `foo` _towards_ +/// the global scope, and not the other way around? +/// +/// Keep in mind that the edge direction specifies the direction in which we can walk the graph to find new variable names. +/// So, if in `foo`'s scope, we want to access a global variable, +/// we can do that by using the edge _towards_ the global scope and find all the variables there. +/// +/// Conversely, in the global scope, we cannot access variables in `foo`'s scope, +/// which is why there is no edge _from_ the global scope _towards_ `foo`'s scope. +/// +///
+/// pub mod edges {} pub mod completeness; From 7dce6de95ff33e7d0ad40f961a81f7070e86c073 Mon Sep 17 00:00:00 2001 From: jdonszelmann Date: Wed, 5 Jun 2024 15:01:55 +0200 Subject: [PATCH 02/14] clippy&fmt --- scopegraphs-render-docs/src/attrs.rs | 20 +++++++++++++------- scopegraphs/src/concepts/mod.rs | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/scopegraphs-render-docs/src/attrs.rs b/scopegraphs-render-docs/src/attrs.rs index dcdcf9b..90b76a7 100644 --- a/scopegraphs-render-docs/src/attrs.rs +++ b/scopegraphs-render-docs/src/attrs.rs @@ -519,12 +519,12 @@ fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec let mut ctx: Ctx<'_> = Default::default(); let flush_buffer_as_doc_comment = |ctx: &mut Ctx| { - // if !ctx.buffer.is_empty() { - ctx.attrs.push(Attr::DocComment( - ident.clone(), - ctx.buffer.drain(..).join(" "), - )); - // } + if !ctx.buffer.is_empty() { + ctx.attrs.push(Attr::DocComment( + ident.clone(), + ctx.buffer.drain(..).join(" "), + )); + } }; let flush_buffer_as_diagram_entry = |ctx: &mut Ctx| { @@ -684,6 +684,9 @@ mod tests { fn check(case: TestCase) { let mut loc = case.location; let attrs = split_attr_body(&case.ident, case.input, &mut loc); + println!("{attrs:?}"); + println!("---"); + println!("{:?}", case.expect_attrs); assert_eq!(loc, case.expect_location); assert_eq!(attrs, case.expect_attrs); } @@ -785,7 +788,10 @@ mod tests { location: Location::InsideDiagram, input: "```", expect_location: Location::OutsideDiagram, - expect_attrs: vec![Attr::DiagramEnd(i())], + expect_attrs: vec![ + Attr::DiagramEntry(i(), "".to_string()), + Attr::DiagramEnd(i()), + ], }; check(case) diff --git a/scopegraphs/src/concepts/mod.rs b/scopegraphs/src/concepts/mod.rs index 82d403e..5dba75f 100644 --- a/scopegraphs/src/concepts/mod.rs +++ b/scopegraphs/src/concepts/mod.rs @@ -397,6 +397,7 @@ pub mod scope_data {} #[render_scopegraphs] /// Edges connect [`Scopes`](crate::concepts::scope) together, based on the rules of the language you're trying to create. +/// /// By adding edges, you're really starting to use scope graphs. /// /// You can think of edges as the path a name resolution can take. Edges are always directed. From 3078445c99f22d4e7d7220f50ae3be0bb4f8618f Mon Sep 17 00:00:00 2001 From: jdonszelmann Date: Wed, 5 Jun 2024 16:29:31 +0200 Subject: [PATCH 03/14] 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