Skip to content

Commit

Permalink
render paths & resolved paths on scopegraphs
Browse files Browse the repository at this point in the history
  • Loading branch information
jdonszelmann committed Jun 6, 2024
1 parent e6eaa73 commit a2dc720
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 22 deletions.
19 changes: 13 additions & 6 deletions scopegraphs-render-docs/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,20 @@ impl quote::ToTokens for Attrs {
.map(Attr::expect_diagram_entry_text)
.collect::<Vec<_>>();

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) => {
Expand Down
154 changes: 150 additions & 4 deletions scopegraphs/src/concepts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand All @@ -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<String> {
/// # 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<Lbl, Data, UncheckedCompleteness> =
/// # 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() {
Expand All @@ -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<String> {
/// # 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<Lbl, Data, UncheckedCompleteness> =
/// # 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() {
Expand Down
63 changes: 51 additions & 12 deletions scopegraphs/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -31,22 +32,25 @@ pub struct RenderSettings {
pub title: Option<String>,
/// 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<ResolvedPath<'sg, LABEL, DATA>>,
}

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<str>) -> Self {
self.title = Some(name.as_ref().to_string());
self
}
}

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,
}
}
}
Expand Down Expand Up @@ -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<W: Write>(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> {
pub fn render<W: Write>(
&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<W: Write>(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> {
fn render_mermaid<W: Write>(
&self,
output: &mut W,
settings: RenderSettings<'_, LABEL, DATA>,
) -> io::Result<()> {
let (mut edges, nodes) = traverse::traverse(self);

if let Some(ref i) = settings.title {
Expand Down Expand Up @@ -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<W: Write>(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> {
fn render_dot<W: Write>(
&self,
output: &mut W,
settings: RenderSettings<'_, LABEL, DATA>,
) -> io::Result<()> {
let (mut edges, nodes) = traverse::traverse(self);

writeln!(output, "digraph {{")?;
Expand Down Expand Up @@ -278,7 +317,7 @@ impl<
pub fn render_to(
&self,
path: impl AsRef<Path>,
mut settings: RenderSettings,
mut settings: RenderSettings<'_, LABEL, DATA>,
) -> io::Result<()> {
let path = path.as_ref();
let mut w = File::create(path)?;
Expand Down
Loading

0 comments on commit a2dc720

Please sign in to comment.