Skip to content

Commit

Permalink
Merge pull request nannou-org#25 from mitchmindtree/expr_push
Browse files Browse the repository at this point in the history
Add `Expr` and `Push` nodes. Add `Edge` constructors. Updated test and example. Push eval and crate name bug fixes.
  • Loading branch information
mitchmindtree authored Mar 27, 2019
2 parents cfacf91 + cc02d33 commit 0f17433
Show file tree
Hide file tree
Showing 8 changed files with 507 additions and 121 deletions.
8 changes: 4 additions & 4 deletions examples/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ impl gantz::Node for Debug {
}

#[typetag::serde]
impl gantz::project::SerdeNode for One {
impl gantz::node::SerdeNode for One {
fn node(&self) -> &gantz::Node { self }
}

#[typetag::serde]
impl gantz::project::SerdeNode for Add {
impl gantz::node::SerdeNode for Add {
fn node(&self) -> &gantz::Node { self }
}

#[typetag::serde]
impl gantz::project::SerdeNode for Debug {
impl gantz::node::SerdeNode for Debug {
fn node(&self) -> &gantz::Node { self }
}

Expand All @@ -84,7 +84,7 @@ fn main() {
let mut project = gantz::Project::open(path.into()).unwrap();

// Instantiate the core nodes.
let one = Box::new(One) as Box<gantz::project::SerdeNode>;
let one = Box::new(One) as Box<gantz::node::SerdeNode>;
let add = Box::new(Add) as Box<_>;
let debug = Box::new(Debug) as Box<_>;

Expand Down
41 changes: 36 additions & 5 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ pub struct Edge {
pub input: node::Input,
}

/// The petgraph type used to represent the **Graph**.
/// The petgraph type used to represent a gantz graph.
pub type Graph<N> = petgraph::Graph<N, Edge, petgraph::Directed, Index>;

/// The petgraph type used to represent a stable gantz graph.
pub type StableGraph<N> = petgraph::stable_graph::StableGraph<N, Edge, petgraph::Directed, Index>;

impl Edge {
/// Create an edge representing a connection from the given node `Output` to the given node
/// `Input`.
pub fn new(output: node::Output, input: node::Input) -> Self {
Edge { output, input }
}
}

impl<N> Node for StableGraph<N>
where
N: Node,
Expand All @@ -35,11 +46,23 @@ where
}
}

impl<A, B> From<(A, B)> for Edge
where
A: Into<node::Output>,
B: Into<node::Input>,
{
fn from((a, b): (A, B)) -> Self {
let output = a.into();
let input = b.into();
Edge { output, input }
}
}

pub mod codegen {
use crate::node::{self, Node};
use petgraph::visit::{Data, EdgeRef, GraphRef, IntoEdgesDirected, IntoNodeReferences,
NodeIndexable, NodeRef, Visitable};
use std::collections::HashMap;
NodeIndexable, NodeRef, Visitable, Walker};
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use super::Edge;
use syn::punctuated::Punctuated;
Expand Down Expand Up @@ -102,16 +125,24 @@ pub mod codegen {
where
G: GraphRef + IntoEdgesDirected + IntoNodeReferences + NodeIndexable + Visitable,
G: Data<EdgeWeight = Edge>,
G::NodeId: Eq + Hash,
<G::NodeRef as NodeRef>::Weight: Node,
{
// First, find all nodes reachable by a `DFS` from this node.
let dfs: HashSet<G::NodeId> = petgraph::visit::Dfs::new(g, n).iter(g).collect();

// The order of evaluation is DFS post order.
let mut dfs_post_order = petgraph::visit::Dfs::new(g, n);
let mut traversal = petgraph::visit::Topo::new(g);

// Track the evaluation steps.
let mut eval_steps = vec![];

// Step through each of the nodes.
while let Some(node) = dfs_post_order.next(g) {
while let Some(node) = traversal.next(g) {
if !dfs.contains(&node) {
continue;
}

// Fetch the node reference.
let child = g.node_references()
.nth(g.to_index(node))
Expand Down
174 changes: 174 additions & 0 deletions src/node/expr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use crate::node::Node;
use proc_macro2::{TokenStream, TokenTree};
use quote::{TokenStreamExt, ToTokens};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

/// A simple node that allows for representing rust expressions as nodes within a gantz graph.
///
/// E.g. the following expression:
///
/// ```ignore
/// #freq.sin() * #amp
/// ```
///
/// will result in a single node with two inputs (`#freq` and `#amp`) and a single output which is
/// the result of the expression.
///
/// ## Limitations
///
/// Currently expressions cannot contain any of the following:
///
/// - Attributes, e.g. `{ #[cfg(target_os = "macos")] { 2 + 2 } }` is a valid expr but not allowed.
/// - Raw strings, e.g. `{ r#"blah blah"# }` is a valid expr but not allowed.
/// - Comments containing the `#` token.
///
/// These limitations are caused by the primitive way in which string interpolation is achieved (we
/// simply count each of the occurrences of `#`). This may be improved in the future.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Expr {
#[serde(with = "crate::node::serde::tts")]
tokens: TokenStream,
}

/// An error occurred while constructing the `Expr` node.
#[derive(Debug, Fail, From)]
pub enum NewExprError {
#[fail(display = "failed to parse the `str` as a valid `TokenStream`")]
InvalidTokenStream,
#[fail(display = "failed to parse the `str` as a valid expr: {}", err)]
InvalidExpr {
#[fail(cause)]
err: syn::Error,
},
}

impl Expr {
/// Construct an **Expr** node from the given rust expression.
///
/// Returns an **Err** if the given string is not a valid expression when interpolated with
/// valid sub-expressions.
///
/// ```rust
/// fn main() {
/// let _node = gantz::node::Expr::new("#foo + #bar").unwrap();
/// }
/// ```
pub fn new(expr: &str) -> Result<Self, NewExprError> {
// Retrieve the `TokenStream`.
let tokens = TokenStream::from_str(expr).map_err(|_| NewExprError::InvalidTokenStream)?;
// Count the number of inputs.
let n_inputs = count_hashes(&tokens);
// Interpolate the `TokenStream` with some temp `{}` expressions.
let unit_expr: syn::Expr = syn::parse_quote!{ {} };
let test_expr_tokens = interpolate_tokens(&tokens, vec![unit_expr; n_inputs as usize]);
let test_expr_str = format!("{}", test_expr_tokens);
let _: syn::Expr = syn::parse_str(&test_expr_str)?;
// If we got this far, we have a valid `Expr`!
Ok(Expr { tokens })
}
}

impl Node for Expr {
fn n_inputs(&self) -> u32 {
count_hashes(&self.tokens)
}

fn n_outputs(&self) -> u32 {
1
}

fn expr(&self, args: Vec<syn::Expr>) -> syn::Expr {
let args_tokens = args.into_iter().map(|expr| expr.into_token_stream());
let expr_tokens = interpolate_tokens(&self.tokens, args_tokens);
syn::parse_quote! { #expr_tokens }
}
}

impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.tokens)
}
}

// A `Punct` instance representing a `#`.
fn hash_punct() -> proc_macro2::Punct {
proc_macro2::Punct::new('#', proc_macro2::Spacing::Alone)
}

// Given a token stream, count all occurrences of `#`.
fn count_hashes<T>(tokens: T) -> u32
where
T: ToTokens,
{
let mut count = 0;
for t in tokens.into_token_stream() {
match t {
TokenTree::Punct(ref p) if format!("{}", p) == format!("{}", hash_punct()) => {
count += 1;
}
TokenTree::Group(ref g) => {
count += count_hashes(g.stream());
}
_ => (),
}
}
count
}

// Given a token stream, sequentially replace each occurrence of `#var` with each expression.
fn interpolate_tokens<T, E>(tokens: T, exprs: E) -> TokenStream
where
T: ToTokens,
E: IntoIterator,
E::Item: ToTokens,
{
fn interpolate_tokens_inner<E>(tokens: TokenStream, exprs: &mut E) -> TokenStream
where
E: Iterator,
E::Item: ToTokens,
{
let mut tokens = tokens.into_iter();
let mut new_tokens = TokenStream::default();
while let Some(t) = tokens.next() {
match t {
TokenTree::Punct(ref p) if format!("{}", p) == format!("{}", hash_punct()) => {
if let Some(expr) = exprs.next() {
tokens.next();
new_tokens.append_all(expr.into_token_stream());
}
}
TokenTree::Group(g) => {
let new_group_tokens = interpolate_tokens_inner(g.stream(), exprs);
let new_group = proc_macro2::Group::new(g.delimiter(), new_group_tokens);
new_tokens.append(new_group);
}
t => new_tokens.append(t),
}
}
new_tokens
}

let tokens = tokens.into_token_stream();
let mut exprs = exprs.into_iter();
interpolate_tokens_inner(tokens, &mut exprs)
}

#[test]
fn test_count_hashes() {
let expr = TokenStream::from_str("#l + #r").unwrap();
assert_eq!(count_hashes(expr), 2);

let expr = TokenStream::from_str("#freq.sin() * #amp").unwrap();
assert_eq!(count_hashes(expr), 2);

let expr = TokenStream::from_str("&#foo").unwrap();
assert_eq!(count_hashes(expr), 1);

let expr = TokenStream::from_str("[#a, #b, #c, #d, #e]").unwrap();
assert_eq!(count_hashes(expr), 5);

let expr = TokenStream::from_str("{}").unwrap();
assert_eq!(count_hashes(expr), 0);
}
31 changes: 30 additions & 1 deletion src/node.rs → src/node/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
pub mod expr;
pub mod push;
pub mod serde;

pub use self::expr::{Expr, NewExprError};
pub use self::push::{Push, WithPushEval};
pub use self::serde::SerdeNode;

/// Gantz allows for constructing executable directed graphs by composing together **Node**s.
///
/// **Node**s are a way to allow users to abstract and encapsulate logic into smaller, re-usable
Expand Down Expand Up @@ -49,13 +57,15 @@ pub trait Node {
}

/// Items that need to be known in order to generate a push evaluation function for a node.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct PushEval {
/// The type for each argument.
#[serde(with = "crate::node::serde::fn_decl")]
pub fn_decl: syn::FnDecl,
/// The name for the function.
pub fn_name: String,
/// Attributes for the generated `ItemFn`.
#[serde(with = "crate::node::serde::fn_attrs")]
pub fn_attrs: Vec<syn::Attribute>,
}

Expand Down Expand Up @@ -134,3 +144,22 @@ impl From<syn::ItemFn> for PushEval {
PushEval { fn_decl, fn_name, fn_attrs }
}
}

impl From<u32> for Input {
fn from(u: u32) -> Self {
Input(u)
}
}

impl From<u32> for Output {
fn from(u: u32) -> Self {
Output(u)
}
}

/// Create a node from the given Rust expression.
///
/// Shorthand for `node::Expr::new`.
pub fn expr(expr: &str) -> Result<Expr, NewExprError> {
Expr::new(expr)
}
Loading

0 comments on commit 0f17433

Please sign in to comment.