Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State #19

Closed
freesig opened this issue Mar 25, 2019 · 4 comments · Fixed by #37
Closed

State #19

freesig opened this issue Mar 25, 2019 · 4 comments · Fixed by #37

Comments

@freesig
Copy link

freesig commented Mar 25, 2019

@mitchmindtree I have an idea for state. Does this work?

// Also generate this struct
struct OnePlusState(node0_state: Node0State, node1_state: Node1State);
pub fn one_push_eval(state: OnePlusState) -> OnePlusState {
    let _node0_output0 = 1;
    let _node1_output0 = { _node0_output0 } + { _node0_output0.clone() };
    let () = println!("{:?}", { _node1_output0 });
}

Then each Node that needs state could also impl fn state(&self) -> syn::Expr where the syn::Expr is the Node0State type etc.
And this function could have a default impl of syn::Expr is ().
Then we can access the state like

let counter = state.0;
counter.count += 1;
state.0 = counter;

Alternatively state could be passed in as &mut but this might cause issues.
This turns each eval function into something similar to nannou's Model -> Model
Then we could make some helper functions for the node to easily get it's state out of the return value.

@freesig
Copy link
Author

freesig commented Mar 25, 2019

Ah I think this won't work because if the function names the types in that tuple then we are back to the same problem. I guess we could use Box instead like you said.

@freesig
Copy link
Author

freesig commented Mar 25, 2019

I guess what's going through my head with this is we know the order of the nodes in the evaluation so it would be nice if we could just use that to in a stack of state.

I think the only way I can imagine this working without any performance penalty is having some sort of buffer that basically emulates a programs stack. You could have a "stack" for every evaluation routine.
However this would require some pretty unsafe casting from bytes to whatever the type is that is required.

We need some way to share memory across crates but the types that are only visible to their crate and opaque to all other crates. Like
[ Stack

  • Opaque Blob
  • MyType
  • Opaque Blob
  • Opaque Blob
  • MyOtherType
    ]

@mitchmindtree
Copy link
Member

mitchmindtree commented Mar 25, 2019

Yeah I agree there needs to be some nice way of providing state to the node's expression that doesn't require a user trying to cast to a potentially incorrect type or something.

Originally I was thinking of using some sort of map like HashMap<NodeIndex, Box<Any>>, but I think you're right that a Vec/Slice is a much better approach for a few reasons:

  1. We don't actually know anything about the node indices in the generated code, so having a map wouldn't really help retrieval of state.
  2. As you say we do know the order of evaluation and we could use that order to associate state with nodes within a slice. It's worth considering how conditional evaluation would effect this but i'd imagine it would still be doable.
  3. Indexing into a slice is a lot faster than hashing which is important for potentially hot code.

Push / Pull Evaluation function argument

In order to get this list of node states into the function, we need to be able to pass in a type that won't require changing no matter how the graph is manipulated as we can't change this function signature at runtime if we need to call it within our application code. As a result, out best option will probably be to use something like &mut [&mut Any], or &mut [Box<Any>] or perhaps some friendlier wrapper type around this and perhaps with some more refined NodeState trait that depends on Any rather than using Any itself.

Casting to the expected type

Once we have this list of trait objects representing each node's state available in the function, we need to know what type to cast them to for each node. This means we need some way for a node to be able to indicate the type that it expects.

Perhaps a node can both opt-in to being "stateful" and describe the state type it expects with a single, optional method on the Node trait that looks something like this:

fn state_type(&self) -> Option<syn::Type>;

where the default implementation returns None, indicating that the node is stateless. If the method returns Some, the node can expect that there will be a local variable called state that has the type &mut T that will be available to their generated expression where T is the type indicated by the state_type method.

A simple Counter node that increments a u32 and returns the result might be implemented something like this:

impl Node for Counter {
    fn state_type(&self) -> Option<syn::Type> {
        let ty: syn::Type = syn::parse_quote! { u32 };
        Some(ty)
    }

    fn expr(&self, _args: Vec<syn::Expr>) -> syn::Expr {
        let expr: syn::Expr = syn::parse_quote! {{
            let count = *state;
            *state += 1;
            count
        }};
        expr
    }

    // Other methods omitted...
}

Simple Example

Here's an annotated snippet of some hypothetical generated code for a simple graph that just has a Button node plugged into our Counter node and our Counter node plugged into a Debug node that demonstrates how this might work:

#[no_mangle]
pub fn button_push_eval(_node_states: &mut [&mut Any]) {
    let () = (); // Button expr does nothing, its only purpose is to support push evaluation.
    let _node1_output0 = {
        // The generated line where we cast from the trait object to the state type expected by the node.
        // We use the index `0` as it's the first stateful node.
        let state: &mut u32 = _node_states[0].downcast_mut::<u32>().expect("some err msg");
        // The expression returned by the counter's `Node::expr` method.
        {
            let count = *state;
            *state += 1;
            count
        }
    };
    let () = println!("{:?}", { _node1_output0 });
}

We could probably provide some nicer type that wraps the &mut [&mut Any] with an API that lets users assemble it a little easier than manually casting, ordering and assembling the slice of state themselves, but at least this looks like it might be a feasible approach?

@freesig what are your thoughts on this direction? I still like your original idea of somehow packing all the state into a single struct and putting it behind a trait object, as it means we would only have to do a single cast for all the node state for a single graph (rather than casting each individual node's state), but I'm not yet sure how or if we can do this in a way that allows the user to be able to actually create this generated struct 🤔

@freesig
Copy link
Author

freesig commented Mar 27, 2019

Yeh I see what you mean about the function signature not being able to change at runtime.
I think this Box approach is probably the best way to move forward. We should wrap it in a nice struct though for two reasons:

  1. It should be easy to use.
  2. If there is any noticeable performance hit from Any (there might not be) then we could looking into making our own unsafe block of memory. So underneath the hood it is just holding * mut void and we do all the type tracking and casting. Although this will be hard and not worth it unless we really need the speed.

The if issue will change the order. I'm not sure how to over come this. I think conditions need there own issue anyway but it would be nice if we could avoid the hashmaping. Perhaps we could make some internal order arbitrary of nodes in an eval path that is the same regardless of eval order. Like

fn eval(state: State) -> State {
    // this is the first time we see a node so it gets position 0
    // The [] operator handles the cast 
    let _node19_output0 = { 1 + state[0].downcast_mut::<Node19StateType>().expect("some err msg") )               };
    // State is [1] for node3 because it's the second node we have seen
    let _node3_output0 = if _node19_output0 { 
        // State is [2] for node5 because it's the third node we have seen
        let _node5_output = {state[2].downcast_mut::<Node5StateType>().expect("some err msg") };
        state[1].downcast_mut::<Node3StateType>().expect("some err msg") + _node5_output
    } else {
         // State is [3] for node22 because it's the fourth node we have seen
        let _node22_output = {state[3].downcast_mut::<Node22StateType>().expect("some err msg") };
        state[1].downcast_mut::<Node3StateType>().expect("some err msg") + _node22_output
    }
        
}

mitchmindtree added a commit to mitchmindtree/gantz that referenced this issue Jun 13, 2019
This allows for optionally specifying the full types of the inputs and
outputs of a `Node` during implementation by allowing to specify a full,
freestanding function, rather than only an expression.

The function's arguments and return type will be parsed to produce the
number of inputs and outputs for the node, where the number of arguments
is the number of inputs, and the number of tuple arguments in the output
is the number of outputs (1 if no tuple output type).

Some of this may have to be re-written when addressing a few follow-up
issues including nannou-org#29, nannou-org#19, nannou-org#21 and nannou-org#22, but I think it's helpful to
break up progress into achievable steps!

Closes nannou-org#27 and makes nannou-org#20 much more feasible.
mitchmindtree added a commit to mitchmindtree/gantz that referenced this issue Jun 13, 2019
This allows for optionally specifying the full types of the inputs and
outputs of a `Node` during implementation by allowing to specify a full,
freestanding function, rather than only an expression.

The function's arguments and return type will be parsed to produce the
number of inputs and outputs for the node, where the number of arguments
is the number of inputs, and the number of tuple arguments in the output
is the number of outputs (1 if no tuple output type).

Some of this may have to be re-written when addressing a few follow-up
issues including nannou-org#29, nannou-org#19, nannou-org#21 and nannou-org#22, but I think it's helpful to
break up progress into achievable steps!

Closes nannou-org#27 and makes nannou-org#20 much more feasible.
mitchmindtree added a commit to mitchmindtree/gantz that referenced this issue Jun 17, 2019
This adds rough support for node state that persists between calls to
evaluation functions.

Currently, node state is made available in evaluation functions via an
added `&mut [&mut dyn std::any::Any]`. Eventually, this should be
changed to a friendlier `node::States` type or something along these
lines, however this will first require some way to make such a type
available to the generated crate. This might mean splitting this type
into its own crate so that it may be shared between both gantz,
generated crates and user crates. For now, the `Any` slice only requires
`std` and seems to work as a basic prototype.

A simple counter.rs test has been added in which a small graph
containing a counter node is composed, compiled, loaded, evaluated three
times and the result is asserted at the end.

I'm not sure if Rust's unstable ABI will begin causing more issues with
larger types, but for now this seems to be working consistently OK with
primitive types on Linux.

This should make it possible to finish nannou-org#20.

Closes nannou-org#19.
mitchmindtree added a commit to mitchmindtree/gantz that referenced this issue Jun 17, 2019
This adds rough support for node state that persists between calls to
evaluation functions.

Currently, node state is made available in evaluation functions via an
added `&mut [&mut dyn std::any::Any]`. Eventually, this should be
changed to a friendlier `node::States` type or something along these
lines, however this will first require some way to make such a type
available to the generated crate. This might mean splitting this type
into its own crate so that it may be shared between both gantz,
generated crates and user crates. For now, the `Any` slice only requires
`std` and seems to work as a basic prototype.

A simple counter.rs test has been added in which a small graph
containing a counter node is composed, compiled, loaded, evaluated three
times and the result is asserted at the end.

I'm not sure if Rust's unstable ABI will begin causing more issues with
larger types, but for now this seems to be working consistently OK with
primitive types on Linux.

This should make it possible to finish nannou-org#20.

Closes nannou-org#19.
mitchmindtree added a commit to mitchmindtree/gantz that referenced this issue Jun 17, 2019
This adds rough support for node state that persists between calls to
evaluation functions.

Currently, node state is made available in evaluation functions via an
added `&mut [&mut dyn std::any::Any]`. Eventually, this should be
changed to a friendlier `node::States` type or something along these
lines, however this will first require some way to make such a type
available to the generated crate. This might mean splitting this type
into its own crate so that it may be shared between both gantz,
generated crates and user crates. For now, the `Any` slice only requires
`std` and seems to work as a basic prototype.

A simple counter.rs test has been added in which a small graph
containing a counter node is composed, compiled, loaded, evaluated three
times and the result is asserted at the end.

I'm not sure if Rust's unstable ABI will begin causing more issues with
larger types, but for now this seems to be working consistently OK with
primitive types on Linux.

This should make it possible to finish nannou-org#20.

Closes nannou-org#19.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants