From 3b943cb828599c96fdf58e01c4c5912a6cd530ae Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 28 Oct 2024 22:17:58 -0400 Subject: [PATCH] Core graph structure --- Cargo.lock | 45 ++++- Cargo.toml | 1 + src/graph/mod.rs | 461 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 4 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 src/graph/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 56f8f66..7685194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,7 +310,7 @@ dependencies = [ "atty", "bitflags", "clap_lex", - "indexmap", + "indexmap 1.9.1", "once_cell", "strsim", "termcolor", @@ -536,6 +536,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "event-listener" version = "2.5.3" @@ -563,6 +569,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flume" version = "0.10.14" @@ -767,7 +779,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.1", "slab", "tokio", "tokio-util", @@ -789,6 +801,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hashlink" version = "0.7.0" @@ -974,6 +992,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + [[package]] name = "inotify" version = "0.9.6" @@ -1471,6 +1499,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.6.0", +] + [[package]] name = "phf" version = "0.10.1" @@ -1940,7 +1978,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 1.9.1", "itoa", "libc", "libsqlite3-sys", @@ -2550,6 +2588,7 @@ dependencies = [ "notify-debouncer-mini", "once_cell", "openssl", + "petgraph", "pulldown-cmark", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 623b36f..4850944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ tree-sitter-rust = "0.20" unicode-normalization = "0.1.19" url = "2.2" uuid = { version = "1.1", features = ["v4" ] } +petgraph = "0.6.5" [target.'cfg(target_os = "ios")'.dependencies] openssl = { version = "0.10", features = ["vendored"] } diff --git a/src/graph/mod.rs b/src/graph/mod.rs new file mode 100644 index 0000000..ccde36d --- /dev/null +++ b/src/graph/mod.rs @@ -0,0 +1,461 @@ +use petgraph::{ + graph::{DiGraph, NodeIndex}, + visit::EdgeRef, +}; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; +use std::{any::Any, collections::VecDeque}; + +type NodeGraph = DiGraph; + +pub struct Graph { + // we treat this as a StableGraph, since nodes are never removed + node_graph: Rc>, + output: Option>, + output_type: std::marker::PhantomData, +} + +impl Graph { + pub fn new() -> Self { + Self { + node_graph: Rc::new(RefCell::new(DiGraph::new())), + output: None, + output_type: std::marker::PhantomData, + } + } + + pub fn set_output + 'static>(&mut self, rule: R) { + assert!(self.output.is_none(), "cannot replace graph output"); + let input = self.add_rule(rule); + self.output = Some(input.node_idx); + } + + fn add_node(&mut self, node: impl Node + 'static) -> Input { + let value = node.value_rc(); + let erased = ErasedNode::new(node); + let idx = self.node_graph.borrow_mut().add_node(erased); + Input { + node_idx: idx, + value, + } + } + + pub fn add_value(&mut self, value: V) -> Input { + return self.add_node(ConstNode(value.clone())); + } + + pub fn add_rule + 'static, V: Clone + 'static>(&mut self, rule: R) -> Input { + return self.add_node(RuleNode::new(rule)); + } + + pub fn add_input(&mut self, mut f: F) -> Input + where + I: ExternalInput + 'static, + I::Output: Clone, + F: FnMut(InvalidationSignal) -> I, + { + let node_idx = Rc::new(Cell::new(None)); + let signal = InvalidationSignal { + node_idx: Rc::clone(&node_idx), + graph: Rc::clone(&self.node_graph), + }; + let input = f(signal); + let node = ExternalInputNode::::new(input); + let input = self.add_node(node); + node_idx.set(Some(input.node_idx)); + input + } + + pub fn freeze(self) -> Result, GraphFreezeError> { + if self.output.is_none() { + return Err(GraphFreezeError::NoOutput); + } + + let graph = self.node_graph.borrow(); + let indices = graph.node_indices(); + drop(graph); + let mut edges = vec![]; + for idx in indices { + let node = &mut self.node_graph.borrow_mut()[idx]; + (node.visit_inputs)(&mut node.any, &mut |input_idx| { + edges.push((input_idx, idx)); + }); + } + + for (source, dest) in edges { + self.node_graph.borrow_mut().add_edge(source, dest, ()); + } + + let sorted = petgraph::algo::toposort(&*self.node_graph.borrow(), None); + if let Err(_cycle) = sorted { + // TODO: actually build a vec describing the cycle path for debugging + return Err(GraphFreezeError::Cyclic(vec![])); + } + + let frozen = FrozenGraph { + node_graph: self.node_graph, + output: self.output.unwrap(), + output_type: std::marker::PhantomData, + }; + + Ok(frozen) + } +} + +#[derive(Debug)] +pub enum GraphFreezeError { + NoOutput, + Cyclic(Vec>), +} + +pub struct FrozenGraph { + node_graph: Rc>, + output: NodeIndex, + output_type: std::marker::PhantomData, +} + +impl FrozenGraph { + fn update_node(&mut self, idx: NodeIndex) { + let graph = self.node_graph.borrow(); + let node = &graph[idx]; + let is_valid = (node.is_valid)(&node.any); + drop(graph); + if !is_valid { + // collect all the edges into a vec so that we can mutably borrow the graph to update the nodes + let edge_sources = self + .node_graph + .borrow() + .edges_directed(idx, petgraph::Direction::Incoming) + .map(|edge| edge.source()) + .collect::>(); + + // Update the dependencies of this node. + // TODO: iterating/recursing here seems less than efficient + // instead, in evaluate, topo sort the graph and update invalid nodes? + for source in edge_sources { + self.update_node(source); + } + + let node = &mut self.node_graph.borrow_mut()[idx]; + // Update the inputs of in this node's struct. + + // Actually update the node's value. + (node.update)(&mut node.any); + } + } + + pub fn evaluate(&mut self) -> Output { + self.update_node(self.output); + let graph = self.node_graph.borrow(); + let node = &graph[self.output].expect_type::(); + node.value() + } +} + +pub struct Input { + node_idx: NodeIndex, + value: Rc>>, +} + +impl Input { + fn value(&self) -> T { + self.value + .as_ref() + .borrow() + .clone() + .expect("Input must be updated before being deref'd") + } +} + +// TODO: there's a lot happening here, make sure this doesn't create a reference cycle +pub struct InvalidationSignal { + node_idx: Rc>>>, + graph: Rc>, +} + +impl InvalidationSignal { + pub fn invalidate(&self) { + let mut graph = self.graph.borrow_mut(); + let mut queue = VecDeque::new(); + queue.push_back(self.node_idx.get().unwrap()); + while let Some(idx) = queue.pop_front() { + let node = &mut graph[idx]; + if (node.is_valid)(&node.any) { + (node.invalidate)(&mut node.any); + let dependents = graph + .edges_directed(idx, petgraph::Direction::Outgoing) + .map(|edge| edge.target()); + queue.extend(dependents); + } + } + } +} + +// TODO: i really want Input to be able to implement Deref somehow + +struct ErasedNode { + any: Box, + is_valid: Box) -> bool>, + invalidate: Box) -> ()>, + visit_inputs: Box, &mut dyn FnMut(NodeIndex) -> ()) -> ()>, + update: Box) -> ()>, +} + +impl ErasedNode { + fn new + 'static, V: 'static>(base: N) -> Self { + // i don't love the double boxing, but i'm not sure how else to do this + let thing: Box> = Box::new(base); + let any: Box = Box::new(thing); + Self { + any, + is_valid: Box::new(|any| { + let x = any.downcast_ref::>>().unwrap(); + x.is_valid() + }), + invalidate: Box::new(|any| { + let x = any.downcast_mut::>>().unwrap(); + x.invalidate(); + }), + visit_inputs: Box::new(|any, visitor| { + let x = any.downcast_mut::>>().unwrap(); + x.visit_inputs(visitor); + }), + update: Box::new(|any| { + let x = any.downcast_mut::>>().unwrap(); + x.update(); + }), + } + } + + // TODO: revisit if these are necessary + fn expect_type<'a, V: 'static>(&'a self) -> &'a dyn Node { + let res = self + .any + .downcast_ref::>>() + .expect("matching node type"); + res.as_ref() + } +} + +trait Node { + fn is_valid(&self) -> bool; + fn invalidate(&mut self); + fn visit_inputs(&mut self, visitor: &mut dyn FnMut(NodeIndex) -> ()); + fn update(&mut self); + // TODO: are these both necessary? + fn value_rc(&self) -> Rc>>; + // TODO: it would be nice if this borrowed and didn't require Clone + fn value(&self) -> Value; +} + +struct ConstNode(V); + +impl Node for ConstNode { + fn is_valid(&self) -> bool { + true + } + + fn invalidate(&mut self) {} + + fn visit_inputs(&mut self, _visitor: &mut dyn FnMut(NodeIndex) -> ()) {} + + fn update(&mut self) {} + + fn value_rc(&self) -> Rc>> { + Rc::new(RefCell::new(Some(self.0.clone()))) + } + + fn value(&self) -> V { + self.0.clone() + } +} + +struct RuleNode { + rule: R, + value: Rc>>, + valid: bool, +} + +impl, V> RuleNode { + fn new(rule: R) -> Self { + Self { + rule, + value: Rc::new(RefCell::new(None)), + valid: false, + } + } +} + +impl + 'static, V: Clone + 'static> Node for RuleNode { + fn is_valid(&self) -> bool { + self.valid + } + + fn invalidate(&mut self) { + self.valid = false; + } + + fn visit_inputs(&mut self, visitor: &mut dyn FnMut(NodeIndex) -> ()) { + struct InputIndexVisitor<'a>(&'a mut dyn FnMut(NodeIndex) -> ()); + impl<'a> InputVisitor for InputIndexVisitor<'a> { + fn visit(&mut self, input: &mut Input) { + self.0(input.node_idx); + } + } + self.rule.visit_inputs(&mut InputIndexVisitor(visitor)); + } + + fn update(&mut self) { + let new_value = self.rule.evaluate(); + *self.value.borrow_mut() = Some(new_value); + } + + fn value_rc(&self) -> Rc>> { + Rc::clone(&self.value) + } + + fn value(&self) -> V { + self.value + .as_ref() + .borrow() + .clone() + .expect("RuleNode must be updated before getting value") + } +} + +struct ExternalInputNode { + input: I, + value: Rc>>, + valid: bool, +} + +impl ExternalInputNode { + fn new(input: I) -> Self { + Self { + input, + value: Rc::new(RefCell::new(None)), + valid: false, + } + } +} + +impl Node for ExternalInputNode { + fn is_valid(&self) -> bool { + self.valid + } + + fn invalidate(&mut self) { + self.valid = false; + } + + fn visit_inputs(&mut self, _visitor: &mut dyn FnMut(NodeIndex) -> ()) {} + + fn update(&mut self) { + self.valid = true; + *self.value.borrow_mut() = Some(self.input.value()); + } + + fn value_rc(&self) -> Rc>> { + Rc::clone(&self.value) + } + + fn value(&self) -> I::Output { + todo!() + } +} + +pub trait Rule { + fn visit_inputs(&mut self, visitor: &mut impl InputVisitor); + + fn evaluate(&mut self) -> Output; +} + +pub trait InputVisitor { + fn visit(&mut self, input: &mut Input); +} + +pub trait ExternalInput { + type Output; + + fn value(&mut self) -> Self::Output; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn erase_node() { + let node = ErasedNode::new(ConstNode(1234 as i32)); + let unwrapped = node.expect_type::(); + assert_eq!(unwrapped.value(), 1234); + } + + struct ConstantRule(i32); + impl Rule for ConstantRule { + fn visit_inputs(&mut self, _visitor: &mut impl InputVisitor) {} + fn evaluate(&mut self) -> i32 { + self.0 + } + } + + #[test] + fn rule_output_with_no_inputs() { + let mut graph = Graph::new(); + graph.set_output(ConstantRule(1234)); + assert_eq!(graph.freeze().unwrap().evaluate(), 1234); + } + + struct Double(Input); + impl Rule for Double { + fn visit_inputs(&mut self, visitor: &mut impl InputVisitor) { + visitor.visit(&mut self.0); + } + fn evaluate(&mut self) -> i32 { + self.0.value() * 2 + } + } + + #[test] + fn rule_with_input() { + let mut graph = Graph::new(); + let input = graph.add_value(42); + graph.set_output(Double(input)); + assert_eq!(graph.freeze().unwrap().evaluate(), 84); + } + + #[test] + fn rule_with_input_rule() { + let mut graph = Graph::new(); + let input = graph.add_value(42); + let doubled = graph.add_rule(Double(input)); + graph.set_output(Double(doubled)); + assert_eq!(graph.freeze().unwrap().evaluate(), 168); + } + + #[test] + fn external_input() { + struct Inc(i32); + impl ExternalInput for Inc { + type Output = i32; + fn value(&mut self) -> i32 { + self.0 += 1; + return self.0; + } + } + let mut graph = Graph::new(); + let mut invalidate = None; + let input = graph.add_input(|inv| { + invalidate = Some(inv); + Inc(0) + }); + graph.set_output(Double(input)); + let mut frozen = graph.freeze().unwrap(); + assert_eq!(frozen.evaluate(), 2); + invalidate.as_ref().unwrap().invalidate(); + assert_eq!(frozen.evaluate(), 4); + assert_eq!(frozen.evaluate(), 4); + invalidate.as_ref().unwrap().invalidate(); + assert_eq!(frozen.evaluate(), 6); + } +} diff --git a/src/main.rs b/src/main.rs index 051845f..5172edd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod activitypub; mod generator; +mod graph; use crate::generator::{HtmlContent, Post}; use axum::{