diff --git a/fud2/fud-core/src/exec/planner.rs b/fud2/fud-core/src/exec/planner.rs index b475d9a9f8..c7775f4979 100644 --- a/fud2/fud-core/src/exec/planner.rs +++ b/fud2/fud-core/src/exec/planner.rs @@ -24,20 +24,160 @@ pub trait FindPlan: std::fmt::Debug { #[derive(Debug, Default)] pub struct EnumeratePlanner {} impl EnumeratePlanner { + /// The max number of ops in a searched for plan. + const MAX_PLAN_LEN: u32 = 7; + pub fn new() -> Self { EnumeratePlanner {} } + + /// Returns `true` if executing `plan` will take `start` to `end`, going through all ops in `through`. + /// + /// This function assumes all required inputs to `plan` exist or will be generated by `plan`. + fn valid_plan(plan: &[Step], end: &[StateRef], through: &[OpRef]) -> bool { + // Check all states in `end` are created. + let end_created = end + .iter() + .all(|s| plan.iter().any(|(_, states)| states.contains(s))); + + // FIXME: Currently this checks that an outputs of an op specified by though is used. + // However, it's possible that the only use of this output by another op whose outputs + // are all unused. This means the plan doesn't actually use the specified op. but this + // code reports it would. + let through_used = through.iter().all(|t| { + plan.iter() + .any(|(op, used_states)| op == t && !used_states.is_empty()) + }); + + end_created && through_used + } + + /// A recursive function to generate all sequences prefixed by `plan` and containing `len` more + /// `Steps`. Returns a sequence such that applying `valid_plan` to the sequence results in `true. + /// If no such sequence exists, then `None` is returned. + /// + /// `start` is the base inputs which can be used for ops. + /// `end` is the states to be generated by the return sequence of ops. + /// `ops` contains all usable operations to construct `Step`s from. + fn try_paths_of_length( + plan: &mut Vec, + len: u32, + start: &[StateRef], + end: &[StateRef], + through: &[OpRef], + ops: &PrimaryMap, + ) -> Option> { + // Base case of the recursion. As `len == 0`, the algorithm reduces to applying `good` to + // `plan. + if len == 0 { + return if Self::valid_plan(plan, end, through) { + Some(plan.clone()) + } else { + None + }; + } + + // Try adding every op to the back of the current `plan`. Then recurse on the subproblem. + for op_ref in ops.keys() { + // Check previous ops in the plan to see if any generated an input to `op_ref`. + let all_generated = ops[op_ref].input.iter().all(|input| { + // Check the outputs of ops earlier in the plan can be used as inputs to `op_ref`. + // `plan` is reversed so the latest versions of states are used. + plan.iter_mut().rev().any(|(o, _used_outputs)| + ops[*o].output.contains(input) + ) + // As well as being generated in `plan`, `input` could be given in `start`. + || start.contains(input) + }); + + // If this op cannot be uesd in the `plan` try a different one. + if !all_generated { + continue; + } + + // Mark all used outputs. + let used_outputs_idxs: Vec<_> = ops[op_ref] + .input + .iter() + .filter_map(|input| { + // Get indicies of `Step`s whose `used_outputs` must be modified. + plan.iter_mut() + .rev() + .position(|(o, used_outputs)| { + // `op_ref`'s op now uses the input of the previous op in the plan. + // This should be noted in `used_outputs`. + !used_outputs.contains(input) + && ops[*o].output.contains(input) + }) + .map(|i| (input, i)) + }) + .collect(); + + for &(&input, i) in &used_outputs_idxs { + plan[i].1.push(input); + } + + // Mark all outputs in `end` as used because they are used (or at least requested) by + // `end`. + let outputs = ops[op_ref].output.clone().into_iter(); + let used_outputs = + outputs.filter(|s| end.contains(s)).collect::>(); + + // Recurse! Now that `len` has been reduced by one, see if this new problem has a + // solution. + plan.push((op_ref, used_outputs)); + if let Some(plan) = Self::try_paths_of_length( + plan, + len - 1, + start, + end, + through, + ops, + ) { + return Some(plan); + } + + // The investigated plan didn't work. + // Pop off the attempted element. + plan.pop(); + + // Revert modifications to `used_outputs`. + for &(_, i) in &used_outputs_idxs { + plan[i].1.pop(); + } + } + + // No sequence of `Step`s found :(. + None + } + /// Returns a sequence of `Step`s to transform `start` to `end`. The `Step`s are guaranteed to /// contain all ops in `through`. If no such sequence exists, `None` is returned. /// /// `ops` is a complete list of operations. fn find_plan( - _start: &[StateRef], - _end: &[StateRef], - _through: &[OpRef], - _ops: &PrimaryMap, + start: &[StateRef], + end: &[StateRef], + through: &[OpRef], + ops: &PrimaryMap, ) -> Option> { - todo!() + // Try all sequences of ops up to `MAX_PATH_LEN`. At that point, the computation starts to + // become really big. + for len in 1..Self::MAX_PLAN_LEN { + if let Some(plan) = Self::try_paths_of_length( + &mut vec![], + len, + start, + end, + through, + ops, + ) { + return Some(plan); + } + } + + // No sequence of `Step`s found :(. + None } } diff --git a/fud2/fud-core/tests/tests.rs b/fud2/fud-core/tests/tests.rs index 8b13789179..f2b2df82dc 100644 --- a/fud2/fud-core/tests/tests.rs +++ b/fud2/fud-core/tests/tests.rs @@ -1 +1,130 @@ +use fud_core::{ + exec::{EnumeratePlanner, FindPlan}, + DriverBuilder, +}; +#[test] +fn find_plan_simple_graph_test() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let s2 = bld.state("s2", &[]); + let t1 = bld.op("t1", &[], s1, s2, |_, _, _| Ok(())); + let driver = bld.build(); + assert_eq!( + Some(vec![(t1, vec![s2])]), + path_finder.find_plan(&[s1], &[s2], &[], &driver.ops) + ); + assert_eq!(None, path_finder.find_plan(&[s1], &[s1], &[], &driver.ops)); +} + +#[test] +fn find_plan_multi_op_graph() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let s2 = bld.state("s2", &[]); + let s3 = bld.state("s3", &[]); + let t1 = bld.op("t1", &[], s1, s3, |_, _, _| Ok(())); + let _ = bld.op("t2", &[], s2, s3, |_, _, _| Ok(())); + let driver = bld.build(); + assert_eq!( + Some(vec![(t1, vec![s3])]), + path_finder.find_plan(&[s1], &[s3], &[], &driver.ops) + ); +} + +#[test] +fn find_plan_multi_path_graph() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let s2 = bld.state("s2", &[]); + let s3 = bld.state("s3", &[]); + let s4 = bld.state("s4", &[]); + let s5 = bld.state("s5", &[]); + let s6 = bld.state("s6", &[]); + let s7 = bld.state("s7", &[]); + let t1 = bld.op("t1", &[], s1, s3, |_, _, _| Ok(())); + let t2 = bld.op("t2", &[], s2, s3, |_, _, _| Ok(())); + let _ = bld.op("t3", &[], s3, s4, |_, _, _| Ok(())); + let t4 = bld.op("t4", &[], s3, s5, |_, _, _| Ok(())); + let t5 = bld.op("t5", &[], s3, s5, |_, _, _| Ok(())); + let _ = bld.op("t6", &[], s6, s7, |_, _, _| Ok(())); + let driver = bld.build(); + assert_eq!( + Some(vec![(t1, vec![s3]), (t4, vec![s5])]), + path_finder.find_plan(&[s1], &[s5], &[], &driver.ops) + ); + assert_eq!( + Some(vec![(t1, vec![s3]), (t5, vec![s5])]), + path_finder.find_plan(&[s1], &[s5], &[t5], &driver.ops) + ); + assert_eq!(None, path_finder.find_plan(&[s6], &[s5], &[], &driver.ops)); + assert_eq!( + None, + path_finder.find_plan(&[s1], &[s5], &[t2], &driver.ops) + ); +} + +#[test] +fn find_plan_only_state_graph() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let driver = bld.build(); + assert_eq!(None, path_finder.find_plan(&[s1], &[s1], &[], &driver.ops)); +} + +#[test] +fn find_plan_self_loop() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let t1 = bld.op("t1", &[], s1, s1, |_, _, _| Ok(())); + let driver = bld.build(); + assert_eq!( + Some(vec![(t1, vec![s1])]), + path_finder.find_plan(&[s1], &[s1], &[t1], &driver.ops) + ); +} + +#[test] +fn find_plan_cycle_graph() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let s2 = bld.state("s2", &[]); + let t1 = bld.op("t1", &[], s1, s2, |_, _, _| Ok(())); + let t2 = bld.op("t2", &[], s2, s1, |_, _, _| Ok(())); + let driver = bld.build(); + assert_eq!( + Some(vec![(t1, vec![s2]), (t2, vec![s1])]), + path_finder.find_plan(&[s1], &[s1], &[], &driver.ops) + ); + assert_eq!( + Some(vec![(t1, vec![s2])]), + path_finder.find_plan(&[s1], &[s2], &[], &driver.ops) + ); + assert_eq!( + Some(vec![(t2, vec![s1])]), + path_finder.find_plan(&[s2], &[s1], &[], &driver.ops) + ); +} + +#[test] +fn find_plan_nontrivial_cycle() { + let path_finder = EnumeratePlanner {}; + let mut bld = DriverBuilder::new("fud2"); + let s1 = bld.state("s1", &[]); + let s2 = bld.state("s2", &[]); + let s3 = bld.state("s3", &[]); + let _t1 = bld.op("t1", &[], s2, s2, |_, _, _| Ok(())); + let t2 = bld.op("t2", &[], s1, s2, |_, _, _| Ok(())); + let t3 = bld.op("t3", &[], s2, s3, |_, _, _| Ok(())); + let driver = bld.build(); + assert_eq!( + Some(vec![(t2, vec![s2]), (t3, vec![s3])]), + path_finder.find_plan(&[s1], &[s3], &[], &driver.ops) + ); +}