diff --git a/Cargo.lock b/Cargo.lock index 0d42daed..97f3e648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2300,6 +2300,7 @@ dependencies = [ "lalrpop", "lalrpop-util", "num-rational 0.4.2", + "num-traits", "path-clean", "rand 0.7.3", "regex", diff --git a/ast/src/ast.rs b/ast/src/ast.rs index 7bdbe8d5..1319fa3b 100644 --- a/ast/src/ast.rs +++ b/ast/src/ast.rs @@ -11,6 +11,7 @@ pub struct FunDef { #[derive(Clone, PartialEq, Debug, Hash)] pub enum Op { + Follow(crate::follow::types::Follow), AsIs, Out, Id(String), diff --git a/ast/src/datagen/mod_1d.rs b/ast/src/datagen/mod_1d.rs index 7e7fb2af..0c9fb9db 100644 --- a/ast/src/datagen/mod_1d.rs +++ b/ast/src/datagen/mod_1d.rs @@ -117,6 +117,7 @@ pub fn eeg_datum_to_point_op( names: nameset, filters: vec![], is_out: false, + follows: vec![], } } diff --git a/ast/src/follow/evaluate/mod.rs b/ast/src/follow/evaluate/mod.rs new file mode 100644 index 00000000..49c102af --- /dev/null +++ b/ast/src/follow/evaluate/mod.rs @@ -0,0 +1,77 @@ +use crate::follow::types::*; + +pub trait EvaluateCondition { + fn eval_bool(&self, f: f32, g: f32) -> bool; +} + +pub trait EvaluateAction { + fn eval_value(&self, f: f32, g: f32) -> (f32, f32); +} + +impl EvaluateCondition for ConditionNF { + fn eval_bool(&self, f: f32, g: f32) -> bool { + match self { + ConditionNF::Comparison(comp) => comp.eval_bool(f, g), + ConditionNF::LogicalAnd(left, right) => left.eval_bool(f, g) && right.eval_bool(f, g), + ConditionNF::LogicalOr(left, right) => left.eval_bool(f, g) || right.eval_bool(f, g), + ConditionNF::LogicalNot(inner) => !inner.eval_bool(f, g), + } + } +} + +impl ComparisonNF { + pub fn eval_bool(&self, f: f32, g: f32) -> bool { + match self { + ComparisonNF::GreaterThan(FollowVariable::F, threshold) => f > *threshold, + ComparisonNF::GreaterThan(FollowVariable::G, threshold) => g > *threshold, + ComparisonNF::LessThan(FollowVariable::F, threshold) => f < *threshold, + ComparisonNF::LessThan(FollowVariable::G, threshold) => g < *threshold, + ComparisonNF::EqualTo(FollowVariable::F, threshold) => (f - *threshold).abs() < 1e-6, + ComparisonNF::EqualTo(FollowVariable::G, threshold) => (g - *threshold).abs() < 1e-6, + } + } +} + +impl EvaluateAction for ActionNF { + fn eval_value(&self, f: f32, g: f32) -> (f32, f32) { + (self.transform_f.apply(f, g), self.transform_g.apply(f, g)) + } +} + +impl FollowNF { + pub fn evaluate(&self, f: f32, g: f32) -> (f32, f32) { + match self { + FollowNF::Condition { + test, + true_branch, + false_branch, + } => { + // Evaluate `test` as a bool. If true, go down `true_branch`, else `false_branch`. + if test.eval_bool(f, g) { + true_branch.evaluate(f, g) + } else { + false_branch.evaluate(f, g) + } + } + FollowNF::Leaf(action) => { + // Evaluate the action => `(f32, f32)`. + action.eval_value(f, g) + } + } + } +} + +impl EvaluateAction for Vec { + fn eval_value(&self, f: f32, g: f32) -> (f32, f32) { + self.iter().fold((f, g), |(acc_f, acc_g), nf| { + let (res_f, res_g) = nf.evaluate(acc_f, acc_g); + (res_f, res_g) + }) + } +} + +impl FollowTransformFn { + pub fn apply(&self, f: f32, g: f32) -> f32 { + (self.0)(f, g) + } +} diff --git a/ast/src/follow/mod.rs b/ast/src/follow/mod.rs new file mode 100644 index 00000000..3426f90a --- /dev/null +++ b/ast/src/follow/mod.rs @@ -0,0 +1,3 @@ +pub mod evaluate; +pub mod normalize; +pub mod types; diff --git a/ast/src/follow/normalize/mod.rs b/ast/src/follow/normalize/mod.rs new file mode 100644 index 00000000..458c2420 --- /dev/null +++ b/ast/src/follow/normalize/mod.rs @@ -0,0 +1,199 @@ +mod test; + +use crate::follow::types::*; +use num_traits::ToPrimitive; + +pub trait ToNF { + fn to_nf(&self, default_action: Option) -> FollowNF; +} + +impl ToNF for Box { + fn to_nf(&self, default_action: Option) -> FollowNF { + (**self).to_nf(default_action) + } +} + +fn to_condition_nf(node: &T) -> ConditionNF { + node.to_nf(None).unwrap_condition() +} + +fn condition_branches_none(test: ConditionNF) -> FollowNF { + FollowNF::Condition { + test, + true_branch: Box::new(FollowNF::Leaf(ActionNF::default())), + false_branch: Box::new(FollowNF::Leaf(ActionNF::default())), + } +} + +impl ToNF for FollowAST { + fn to_nf(&self, default_action: Option) -> FollowNF { + match self { + FollowAST::Follow(follow) => follow.to_nf(default_action), + FollowAST::Rule(rule) => rule.to_nf(default_action), + FollowAST::Condition(condition) => condition.to_nf(default_action), + FollowAST::AndCondition(and_condition) => and_condition.to_nf(default_action), + FollowAST::SimpleCondition(simple_condition) => simple_condition.to_nf(default_action), + FollowAST::Comparison(comparison) => comparison.to_nf(default_action), + FollowAST::Action(action) => action.to_nf(default_action), + } + } +} + +impl Program { + pub fn to_nf(&self, default_action: Option) -> Vec { + let default_action = default_action.unwrap_or_default(); + + self.follows + .iter() + .map(|follow| follow.to_nf(Some(default_action.clone()))) + .collect() + } +} + +impl ToNF for Follow { + fn to_nf(&self, default_action: Option) -> FollowNF { + let default_action = default_action.unwrap_or_default(); + + // Fold from the right, constructing a nested conditional chain. + let tree = self.rules.iter().rev().fold(None, |acc, rule| { + let subtree = rule.to_nf(Some(default_action.clone())); + Some(match acc { + Some(existing_tree) => match subtree { + FollowNF::Condition { + test, true_branch, .. + } => FollowNF::Condition { + test, + true_branch, + false_branch: Box::new(existing_tree), + }, + FollowNF::Leaf(_) => { + panic!("Unexpected Leaf node during Follow::to_nf: {:?}", subtree); + } + }, + None => subtree, + }) + }); + + // If no rules, use the default action + tree.unwrap_or_else(|| FollowNF::Leaf(default_action)) + } +} + +impl ToNF for FollowRule { + fn to_nf(&self, default_action: Option) -> FollowNF { + match self { + FollowRule::Conditional { condition, action } => { + let test = to_condition_nf(condition); + let true_branch = action.to_nf(None); + let false_branch = FollowNF::Leaf(default_action.unwrap_or_default()); + + FollowNF::Condition { + test, + true_branch: Box::new(true_branch), + false_branch: Box::new(false_branch), + } + } + FollowRule::Default(action) => FollowNF::Leaf(ActionNF { + transform_f: expr_to_transform(&action.expr_f), + transform_g: expr_to_transform(&action.expr_g), + }), + } + } +} + +impl ToNF for FollowCondition { + fn to_nf(&self, _default_action: Option) -> FollowNF { + match self { + FollowCondition::Or(left, right) => { + let test = ConditionNF::LogicalOr( + Box::new(to_condition_nf(left)), + Box::new(to_condition_nf(right)), + ); + condition_branches_none(test) + } + FollowCondition::And(and_condition) => and_condition.to_nf(None), + } + } +} + +impl ToNF for FollowAndCondition { + fn to_nf(&self, _default_action: Option) -> FollowNF { + match self { + FollowAndCondition::And(left, right) => { + let test = ConditionNF::LogicalAnd( + Box::new(to_condition_nf(left)), + Box::new(to_condition_nf(right)), + ); + condition_branches_none(test) + } + FollowAndCondition::Simple(simple_condition) => simple_condition.to_nf(None), + } + } +} + +impl ToNF for FollowSimpleCondition { + fn to_nf(&self, _default_action: Option) -> FollowNF { + match self { + FollowSimpleCondition::Comparison(comp) => comp.to_nf(None), + FollowSimpleCondition::Nested(inner) => { + // `LogicalNot` around the inner condition + let test = ConditionNF::LogicalNot(Box::new(to_condition_nf(inner))); + condition_branches_none(test) + } + } + } +} + +impl ToNF for FollowComparison { + fn to_nf(&self, _default_action: Option) -> FollowNF { + let comparison_nf = match self { + FollowComparison::GreaterThan(var, value) => { + ComparisonNF::GreaterThan(*var, value.to_f32().unwrap()) + } + FollowComparison::LessThan(var, value) => { + ComparisonNF::LessThan(*var, value.to_f32().unwrap()) + } + FollowComparison::EqualTo(var, value) => { + ComparisonNF::EqualTo(*var, value.to_f32().unwrap()) + } + }; + + FollowNF::Condition { + test: ConditionNF::Comparison(comparison_nf), + true_branch: Box::new(FollowNF::Leaf(ActionNF::default())), + false_branch: Box::new(FollowNF::Leaf(ActionNF::default())), + } + } +} + +impl ToNF for FollowAction { + fn to_nf(&self, _default_action: Option) -> FollowNF { + FollowNF::Leaf(ActionNF { + transform_f: expr_to_transform(&self.expr_f), + transform_g: expr_to_transform(&self.expr_g), + }) + } +} + +fn expr_to_transform(expr: &FollowExpr) -> FollowTransformFn { + match expr { + FollowExpr::Value(value) => { + let val = value.to_f32().unwrap(); + FollowTransformFn::new(move |_, _| val) + } + FollowExpr::Variable(var) => match var { + FollowVariable::F => FollowTransformFn::new(|f, _| f), + FollowVariable::G => FollowTransformFn::new(|_, g| g), + }, + } +} + +impl FollowNF { + /// Extract the condition if it's a `Condition`, otherwise panic. + pub fn unwrap_condition(&self) -> ConditionNF { + match self { + FollowNF::Condition { test, .. } => test.clone(), + other => panic!("Expected FollowNF::Condition but found {:?}", other), + } + } +} diff --git a/ast/src/follow/normalize/test.rs b/ast/src/follow/normalize/test.rs new file mode 100644 index 00000000..aeae596a --- /dev/null +++ b/ast/src/follow/normalize/test.rs @@ -0,0 +1,225 @@ +#[cfg(test)] +mod tests { + + use crate::types::*; + use num_rational::Rational64; + + #[test] + fn test_single_conditional_rule() { + let program = Program { + follows: vec![Follow { + rules: vec![FollowRule::Conditional { + condition: FollowCondition::And(FollowAndCondition::Simple( + FollowSimpleCondition::Comparison(FollowComparison::GreaterThan( + FollowVariable::F, + Rational64::new(2, 1), + )), + )), + action: FollowAction { + expr_f: FollowExpr::Variable(FollowVariable::F), + expr_g: FollowExpr::Value(Rational64::new(4, 1)), + }, + }], + }], + }; + + let expected = FollowNF::Condition { + test: ConditionNF::Comparison(ComparisonNF::GreaterThan(FollowVariable::F, 2.0)), + true_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|f, _| f), + transform_g: FollowTransformFn::new(|_, _| 4.0), + })), + false_branch: Box::new(FollowNF::Leaf(ActionNF::default())), + }; + + let default_action = ActionNF::default(); + let compiled = program.to_nf(Some(default_action.clone())); + + assert_eq!(compiled[0], expected); + } + + #[test] + fn test_default_rule() { + let program = Program { + follows: vec![Follow { + rules: vec![FollowRule::Default(FollowAction { + expr_f: FollowExpr::Value(Rational64::new(0, 1)), + expr_g: FollowExpr::Value(Rational64::new(0, 1)), + })], + }], + }; + + let expected = FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|_, _| 0.0), + transform_g: FollowTransformFn::new(|_, _| 0.0), + }); + + let default_action = ActionNF::default(); + let compiled = program.to_nf(Some(default_action.clone())); + + assert_eq!(compiled[0], expected); + } + + #[test] + fn test_multiple_rules() { + let program = Program { + follows: vec![Follow { + rules: vec![ + FollowRule::Conditional { + condition: FollowCondition::And(FollowAndCondition::Simple( + FollowSimpleCondition::Comparison(FollowComparison::GreaterThan( + FollowVariable::F, + Rational64::new(2, 1), + )), + )), + action: FollowAction { + expr_f: FollowExpr::Variable(FollowVariable::F), + expr_g: FollowExpr::Value(Rational64::new(4, 1)), + }, + }, + FollowRule::Default(FollowAction { + expr_f: FollowExpr::Value(Rational64::new(0, 1)), + expr_g: FollowExpr::Value(Rational64::new(0, 1)), + }), + ], + }], + }; + + let expected = FollowNF::Condition { + test: ConditionNF::Comparison(ComparisonNF::GreaterThan(FollowVariable::F, 2.0)), + true_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|f, _| f), + transform_g: FollowTransformFn::new(|_, _| 4.0), + })), + false_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|_, _| 0.0), + transform_g: FollowTransformFn::new(|_, _| 0.0), + })), + }; + + let default_action = ActionNF::default(); + let compiled = program.to_nf(Some(default_action.clone())); + + assert_eq!(compiled[0], expected); + } + + #[test] + fn test_and() { + let program = Program { + follows: vec![Follow { + rules: vec![ + FollowRule::Conditional { + condition: FollowCondition::And(FollowAndCondition::And( + Box::new(FollowAndCondition::Simple( + FollowSimpleCondition::Comparison(FollowComparison::GreaterThan( + FollowVariable::F, + Rational64::new(2, 1), + )), + )), + Box::new(FollowSimpleCondition::Comparison( + FollowComparison::LessThan( + FollowVariable::G, + Rational64::new(3, 1), + ), + )), + )), + action: FollowAction { + expr_f: FollowExpr::Variable(FollowVariable::F), + expr_g: FollowExpr::Value(Rational64::new(4, 1)), + }, + }, + FollowRule::Default(FollowAction { + expr_f: FollowExpr::Value(Rational64::new(0, 1)), + expr_g: FollowExpr::Value(Rational64::new(0, 1)), + }), + ], + }], + }; + + let expected = FollowNF::Condition { + test: ConditionNF::LogicalAnd( + Box::new(ConditionNF::Comparison(ComparisonNF::GreaterThan( + FollowVariable::F, + 2.0, + ))), + Box::new(ConditionNF::Comparison(ComparisonNF::LessThan( + FollowVariable::G, + 3.0, + ))), + ), + true_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|f, _| f), + transform_g: FollowTransformFn::new(|_, _| 4.0), + })), + false_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|_, _| 0.0), + transform_g: FollowTransformFn::new(|_, _| 0.0), + })), + }; + + let default_action = ActionNF::default(); + let compiled = program.to_nf(Some(default_action.clone())); + + assert_eq!(compiled[0], expected); + } + + #[test] + fn test_or() { + let program = Program { + follows: vec![Follow { + rules: vec![ + FollowRule::Conditional { + condition: FollowCondition::Or( + Box::new(FollowCondition::And(FollowAndCondition::Simple( + FollowSimpleCondition::Comparison(FollowComparison::GreaterThan( + FollowVariable::F, + Rational64::new(2, 1), + )), + ))), + Box::new(FollowAndCondition::Simple( + FollowSimpleCondition::Comparison(FollowComparison::LessThan( + FollowVariable::G, + Rational64::new(3, 1), + )), + )), + ), + action: FollowAction { + expr_f: FollowExpr::Variable(FollowVariable::F), + expr_g: FollowExpr::Value(Rational64::new(4, 1)), + }, + }, + FollowRule::Default(FollowAction { + expr_f: FollowExpr::Value(Rational64::new(0, 1)), + expr_g: FollowExpr::Value(Rational64::new(0, 1)), + }), + ], + }], + }; + + let expected = FollowNF::Condition { + test: ConditionNF::LogicalOr( + Box::new(ConditionNF::Comparison(ComparisonNF::GreaterThan( + FollowVariable::F, + 2.0, + ))), + Box::new(ConditionNF::Comparison(ComparisonNF::LessThan( + FollowVariable::G, + 3.0, + ))), + ), + true_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|f, _| f), + transform_g: FollowTransformFn::new(|_, _| 4.0), + })), + false_branch: Box::new(FollowNF::Leaf(ActionNF { + transform_f: FollowTransformFn::new(|_, _| 0.0), + transform_g: FollowTransformFn::new(|_, _| 0.0), + })), + }; + + let default_action = ActionNF::default(); + let compiled = program.to_nf(Some(default_action.clone())); + + assert_eq!(compiled[0], expected); + } +} diff --git a/ast/src/follow/types.rs b/ast/src/follow/types.rs new file mode 100644 index 00000000..bbf0ff06 --- /dev/null +++ b/ast/src/follow/types.rs @@ -0,0 +1,253 @@ +use num_rational::Rational64; +use std::cmp::*; +use std::fmt; +use std::hash::*; +use std::sync::Arc; + +#[derive(Debug, Clone, PartialEq)] +#[allow(dead_code)] +pub enum FollowAST { + Follow(Follow), + Rule(FollowRule), + Condition(FollowCondition), + AndCondition(FollowAndCondition), + SimpleCondition(FollowSimpleCondition), + Comparison(FollowComparison), + Action(FollowAction), +} + +#[derive(Debug, Clone, PartialEq)] +#[allow(dead_code)] +pub enum CompiledFollowAST { + FollowNF(FollowNF), + ConditionNF(ConditionNF), + ComparisonNF(ComparisonNF), + ActionNF(ActionNF), + TransformFn(FollowTransformFn), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Program { + pub follows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Follow { + pub rules: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FollowRule { + Conditional { + condition: FollowCondition, + action: FollowAction, + }, + Default(FollowAction), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FollowCondition { + Or(Box, Box), + And(FollowAndCondition), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FollowAndCondition { + And(Box, Box), + Simple(FollowSimpleCondition), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FollowSimpleCondition { + Comparison(FollowComparison), + Nested(Box), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FollowComparison { + GreaterThan(FollowVariable, Rational64), + LessThan(FollowVariable, Rational64), + EqualTo(FollowVariable, Rational64), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] +pub enum FollowVariable { + F, + G, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FollowAction { + pub expr_f: FollowExpr, + pub expr_g: FollowExpr, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FollowExpr { + Value(Rational64), + Variable(FollowVariable), +} + +#[derive(Clone)] +pub struct FollowTransformFn(pub Arc f32 + Send + Sync>); + +impl FollowTransformFn { + pub fn new(func: F) -> Self + where + F: Fn(f32, f32) -> f32 + 'static + Send + Sync, + { + Self(Arc::new(func)) + } +} + +impl serde::Serialize for FollowTransformFn { + fn serialize(&self, s: S) -> Result { + s.serialize_str("TransformFn()") + } +} + +impl<'a> serde::Deserialize<'a> for FollowTransformFn { + fn deserialize>(d: D) -> Result { + Ok(FollowTransformFn::new(|_, _| 1.0)) + } +} + +impl Eq for FollowTransformFn {} + +impl fmt::Debug for FollowTransformFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TransformFn()") + } +} + +fn sample_points() -> &'static [(f32, f32)] { + &[(1.0, 2.0), (0.0, 0.0), (3.0, -1.0)] +} + +impl PartialEq for FollowTransformFn { + fn eq(&self, other: &Self) -> bool { + sample_points() + .iter() + .all(|&(x, y)| (self.0)(x, y) == (other.0)(x, y)) + } +} + +// Implement Hash by hashing the outputs at the same sample points. +// We need some sort of best-effort approach here. +impl std::hash::Hash for FollowTransformFn { + fn hash(&self, state: &mut H) { + for &(x, y) in sample_points() { + // Convert the output float to bits to have a stable representation. + // This is naive but at least consistent. + let output_bits = (self.0)(x, y).to_bits(); + output_bits.hash(state); + } + } +} + +impl PartialOrd for FollowTransformFn { + fn partial_cmp(&self, other: &Self) -> Option { + for &(x, y) in sample_points() { + let lhs = (self.0)(x, y); + let rhs = (other.0)(x, y); + + match lhs.partial_cmp(&rhs) { + Some(std::cmp::Ordering::Equal) => continue, + non_eq => return non_eq, + } + } + Some(std::cmp::Ordering::Equal) + } +} + +impl Ord for FollowTransformFn { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) + } +} + +#[derive(Debug, Clone, PartialEq, Ord, PartialOrd, Eq, Hash, Serialize, Deserialize)] +pub enum FollowNF { + Condition { + test: ConditionNF, + true_branch: Box, + false_branch: Box, + }, + Leaf(ActionNF), +} + +/// Normal form for conditions +#[derive(Debug, Clone, PartialEq, Ord, PartialOrd, Eq, Hash, Serialize, Deserialize)] +pub enum ConditionNF { + Comparison(ComparisonNF), + LogicalAnd(Box, Box), + LogicalOr(Box, Box), + LogicalNot(Box), +} + +/// Comparison variants +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ComparisonNF { + GreaterThan(FollowVariable, f32), + LessThan(FollowVariable, f32), + EqualTo(FollowVariable, f32), +} +// We can do a naive custom approach: +impl PartialEq for ComparisonNF { + fn eq(&self, other: &Self) -> bool { + // compare both the enum discriminant and the float bits + use ComparisonNF::*; + match (self, other) { + (GreaterThan(a1, f1), GreaterThan(a2, f2)) => a1 == a2 && f1.to_bits() == f2.to_bits(), + (LessThan(a1, f1), LessThan(a2, f2)) => a1 == a2 && f1.to_bits() == f2.to_bits(), + (EqualTo(a1, f1), EqualTo(a2, f2)) => a1 == a2 && f1.to_bits() == f2.to_bits(), + _ => false, + } + } +} + +impl Eq for ComparisonNF {} + +// A "fake" total ordering that compares float bits, ignoring NaNs or special cases +use std::cmp::Ordering; + +impl PartialOrd for ComparisonNF { + fn partial_cmp(&self, _other: &Self) -> Option { + Some(Ordering::Equal) + } +} + +impl Ord for ComparisonNF { + fn cmp(&self, _other: &Self) -> Ordering { + Ordering::Equal + } +} + +impl Hash for ComparisonNF { + fn hash(&self, state: &mut H) { + use ComparisonNF::*; + std::mem::discriminant(self).hash(state); + match self { + GreaterThan(a, x) | LessThan(a, x) | EqualTo(a, x) => { + a.hash(state); + x.to_bits().hash(state); + } + } + } +} + +/// NF for actions +#[derive(Debug, Clone, PartialEq, Ord, PartialOrd, Eq, Hash, Serialize, Deserialize)] +pub struct ActionNF { + pub transform_f: FollowTransformFn, + pub transform_g: FollowTransformFn, +} + +impl Default for ActionNF { + fn default() -> Self { + ActionNF { + transform_f: FollowTransformFn::new(|_, _| 1.0), + transform_g: FollowTransformFn::new(|_, _| 1.0), + } + } +} diff --git a/ast/src/lib.rs b/ast/src/lib.rs index ca25f972..f2fd49f8 100644 --- a/ast/src/lib.rs +++ b/ast/src/lib.rs @@ -2,6 +2,7 @@ extern crate serde; pub mod ast; pub mod datagen; +pub mod follow; pub mod generator; pub mod lists; pub mod nameset; diff --git a/ast/src/operations/get_length_ratio.rs b/ast/src/operations/get_length_ratio.rs index 27592c29..86541665 100644 --- a/ast/src/operations/get_length_ratio.rs +++ b/ast/src/operations/get_length_ratio.rs @@ -13,6 +13,7 @@ impl GetLengthRatio for Op { ) -> Result { match self { Op::AsIs {} + | Op::Follow(..) | Op::Out {} | Op::Lowpass { .. } | Op::FMOsc { .. } diff --git a/ast/src/operations/helpers.rs b/ast/src/operations/helpers.rs index 79526d30..4430a79c 100644 --- a/ast/src/operations/helpers.rs +++ b/ast/src/operations/helpers.rs @@ -83,6 +83,7 @@ pub fn pad_length( names: NameSet::new(), filters: vec![], is_out: false, + follows: vec![], }); } } @@ -116,6 +117,7 @@ pub fn join_sequence(mut l: NormalForm, mut r: NormalForm) -> NormalForm { names: NameSet::new(), filters: vec![], is_out: false, + follows: vec![], }]) } } @@ -137,6 +139,7 @@ pub fn join_sequence(mut l: NormalForm, mut r: NormalForm) -> NormalForm { names: NameSet::new(), filters: vec![], is_out: false, + follows: vec![], }]) } } diff --git a/ast/src/operations/mod.rs b/ast/src/operations/mod.rs index 85ebeb33..bc27d6c8 100644 --- a/ast/src/operations/mod.rs +++ b/ast/src/operations/mod.rs @@ -52,6 +52,7 @@ pub struct PointOp { pub filters: Vec, /// Should fade out to nothing pub is_out: bool, + pub follows: Vec, } impl Default for PointOp { @@ -72,6 +73,7 @@ impl Default for PointOp { names: NameSet::new(), filters: vec![], is_out: false, + follows: vec![], } } } @@ -233,6 +235,12 @@ impl Mul for PointOp { .map(|f| f.to_owned()) .collect(), is_out: other.is_out, + follows: self + .follows + .iter() + .cloned() + .chain(other.follows.iter().cloned()) + .collect(), } } } @@ -272,6 +280,12 @@ impl<'a> Mul<&'a PointOp> for &PointOp { .map(|f| f.to_owned()) .collect(), is_out: other.is_out, + follows: self + .follows + .iter() + .cloned() + .chain(other.follows.iter().cloned()) + .collect(), } } } @@ -309,6 +323,12 @@ impl MulAssign for PointOp { .map(|f| f.to_owned()) .collect(), is_out: other.is_out, + follows: self + .follows + .iter() + .cloned() + .chain(other.follows.iter().cloned()) + .collect(), } } } @@ -356,6 +376,12 @@ impl PointOp { .map(|f| f.to_owned()) .collect(), is_out: other.is_out, + follows: self + .follows + .iter() + .cloned() + .chain(other.follows.iter().cloned()) + .collect(), } } @@ -376,6 +402,7 @@ impl PointOp { names: NameSet::new(), filters: vec![], is_out: false, + follows: vec![], } } pub fn init_silent() -> PointOp { @@ -395,6 +422,7 @@ impl PointOp { names: NameSet::new(), filters: vec![], is_out: false, + follows: vec![], } } diff --git a/ast/src/operations/normalize.rs b/ast/src/operations/normalize.rs index 459aed57..d24df69c 100644 --- a/ast/src/operations/normalize.rs +++ b/ast/src/operations/normalize.rs @@ -1,4 +1,5 @@ use crate::datagen::{csv2d_to_normalform, mod_1d::csv1d_to_normalform}; +use crate::follow::normalize::ToNF; use crate::operations::Rational64; use crate::operations::{ helpers::*, substitute::insert_function_args, GetLengthRatio, NormalForm, Normalize, Substitute, @@ -19,6 +20,12 @@ impl Normalize for Op { defs: &mut Defs, ) -> Result<(), Error> { match self { + Op::Follow(follow) => { + let fnf = follow.to_nf(Some(Default::default())); + input.fmap_mut(|op| { + op.follows.push(fnf.clone()); + }); + } Op::AsIs => {} Op::Out => { input.fmap_mut(|op| { diff --git a/core/src/generation/timed_op.rs b/core/src/generation/timed_op.rs index 4d1040ba..407326f2 100644 --- a/core/src/generation/timed_op.rs +++ b/core/src/generation/timed_op.rs @@ -77,6 +77,7 @@ impl TimedOp { filters: Vec::new(), //TODO is_out: false, + follows: vec![], } } diff --git a/core/src/manager/render_manager.rs b/core/src/manager/render_manager.rs index 62af017c..c88de9aa 100644 --- a/core/src/manager/render_manager.rs +++ b/core/src/manager/render_manager.rs @@ -10,6 +10,7 @@ use log::info; use opmap::OpMap; use std::sync::mpsc::Sender; use std::{path::PathBuf, sync::mpsc::SendError}; +use weresocool_ast::follow::evaluate::EvaluateAction; use weresocool_error::Error; use weresocool_instrument::renderable::{ nf_to_vec_renderable, renderables_to_render_voices, Offset, RenderOp, RenderVoice, Renderable, @@ -179,7 +180,7 @@ impl RenderManager { store.iter_mut().zip(to_store).for_each(|(voice, ops)| { voice.extend(ops.into_iter().map(|mut op| { - op.follow = false; + op.follows = vec![]; op })); }); @@ -271,13 +272,15 @@ impl RenderManager { .filter(|op| op.index % 6 == 0) .cloned() .map(|mut op| { - if op.follow { - op.f *= offset.freq; - op.g = ( - op.g.0 * offset.gain, - op.g.1 * offset.gain, - ); - } + let follow_offset = op.follows.eval_value( + offset.freq as f32, + offset.gain as f32, + ); + op.f *= follow_offset.0 as f64; + op.g = ( + op.g.0 * follow_offset.1 as f64, + op.g.1 * follow_offset.1 as f64, + ); op }) .collect(); diff --git a/core/src/renderable/mod.rs b/core/src/renderable/mod.rs index 1953afed..fa93b1f5 100644 --- a/core/src/renderable/mod.rs +++ b/core/src/renderable/mod.rs @@ -107,7 +107,7 @@ mod tests { names: vec![], filters: vec![], next_out: false, - follow: true, + follows: true, }, RenderOp { f: 330.0, @@ -131,7 +131,7 @@ mod tests { names: vec![], filters: vec![], next_out: false, - follow: true, + follows: true, }, RenderOp { f: 0.0, @@ -155,7 +155,7 @@ mod tests { names: vec![], filters: vec![], next_out: false, - follow: true, + follows: true, }, ]]; assert_eq!(result, expected); diff --git a/instrument/src/renderable/mod.rs b/instrument/src/renderable/mod.rs index cbf1c993..dcea8ff4 100644 --- a/instrument/src/renderable/mod.rs +++ b/instrument/src/renderable/mod.rs @@ -8,7 +8,10 @@ use rand::{thread_rng, Rng}; pub use render_voice::{renderables_to_render_voices, RenderVoice}; use scop::Defs; use serde::{Deserialize, Serialize}; -use weresocool_ast::{NormalForm, Normalize, OscType, PointOp, Term, ASR}; +use weresocool_ast::{ + follow::evaluate::EvaluateAction, follow::types::FollowNF, NormalForm, Normalize, OscType, + PointOp, Term, ASR, +}; use weresocool_error::Error; use weresocool_filter::BiquadFilterDef; pub(crate) use weresocool_shared::{lossy_rational_mul, r_to_f64, Settings}; @@ -37,7 +40,7 @@ pub struct RenderOp { pub names: Vec, pub filters: Vec, pub next_out: bool, - pub follow: bool, + pub follows: Vec, } impl RenderOp { @@ -64,7 +67,7 @@ impl RenderOp { next_out: false, names: Vec::new(), filters: Vec::new(), - follow: true, + follows: Vec::new(), } } @@ -91,7 +94,7 @@ impl RenderOp { next_out: false, names: Vec::new(), filters: Vec::new(), - follow: true, + follows: Vec::new(), } } pub fn init_silent_with_length(l: f64) -> Self { @@ -117,7 +120,7 @@ impl RenderOp { next_out: false, names: Vec::new(), filters: Vec::new(), - follow: true, + follows: Vec::new(), } } @@ -150,7 +153,7 @@ impl RenderOp { next_out: false, names: vec![], filters, - follow: true, + follows: vec![], } } } @@ -183,13 +186,10 @@ impl Renderable for RenderOp { fn render(&mut self, oscillator: &mut Oscillator, offset: Option<&Offset>) -> StereoWaveform { let o = match offset { Some(o) => { - if self.follow { - Offset { - freq: o.freq, - gain: o.gain, - } - } else { - Offset::default() + let (f, g) = self.follows.eval_value(o.freq as f32, o.gain as f32); + Offset { + freq: f as f64, + gain: g as f64, } } None => Offset::default(), @@ -281,7 +281,7 @@ fn pointop_to_renderop( }) .collect(), next_out, - follow: true, + follows: point_op.follows.clone(), }; *time += point_op.l * basis.l; diff --git a/instrument/src/renderable/render_voice.rs b/instrument/src/renderable/render_voice.rs index 8c08d4cb..856f2086 100644 --- a/instrument/src/renderable/render_voice.rs +++ b/instrument/src/renderable/render_voice.rs @@ -1,3 +1,5 @@ +use weresocool_ast::follow::types::FollowNF; + use crate::renderable::{Offset, RenderOp, Renderable}; use crate::{Oscillator, StereoWaveform}; @@ -62,6 +64,7 @@ impl RenderVoice { names: current_op.names.clone(), filters: current_op.filters.clone(), osc_type: current_op.osc_type.clone(), + follows: current_op.follows.clone(), ..*current_op }); self.sample_index += samples_left_in_batch; @@ -73,6 +76,7 @@ impl RenderVoice { names: current_op.names.clone(), filters: current_op.filters.clone(), osc_type: current_op.osc_type.clone(), + follows: current_op.follows.clone(), ..*current_op }); diff --git a/parser/Cargo.toml b/parser/Cargo.toml index ae1d1243..6f2e7996 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -25,6 +25,7 @@ indexmap = "2.0.0" rand = { version="0.7.3", features=["wasm-bindgen"]} path-clean = "0.1.0" uuid = { version = "0.8", features = ["serde", "v4","stdweb"] } +num-traits = "0.2.19" [features] default=["app"] diff --git a/parser/src/socool.lalrpop b/parser/src/socool.lalrpop index ce420763..0fbf5fe5 100644 --- a/parser/src/socool.lalrpop +++ b/parser/src/socool.lalrpop @@ -21,6 +21,9 @@ use weresocool_ast::{ Scale, FmOscDef }; +use weresocool_ast::follow::types::{ + Follow, FollowRule, FollowCondition, FollowAction, FollowVariable, FollowExpr, FollowComparison, FollowSimpleCondition, FollowAndCondition +}; use crate::parser::{Init, handle_fit_length_recursively}; use crate::indices::{et, random_seed}; use crate::float_to_rational::helpers::*; @@ -257,6 +260,7 @@ BaseOperation: Term = { r"\\|Lambda" ?> "{" "}" => Term::Op(Lambda { term: Box::new(term), input_name, scope: uuid::Uuid::new_v4().to_string()}), "(" ")" => o, + => Term::Op(Follow(follow)), => Term::Op(TransposeM {m}), => Term::Op(TransposeA {a}), => Term::Op(PanM {m}), @@ -423,6 +427,50 @@ CoefStart: (i64, i64) = { "|" => (n, d), } +Follow: Follow = { + "Follow {" > "}" => Follow { rules } +}; + +FollowRule: FollowRule = { + "->" => FollowRule::Conditional { condition: cond, action: act }, + "_" "->" => FollowRule::Default(act), +}; + +FollowCondition: FollowCondition = { + "||" => FollowCondition::Or(Box::new(left), Box::new(right)), + => FollowCondition::And(and), +}; + +FollowAndCondition: FollowAndCondition = { + "&&" => FollowAndCondition::And(Box::new(left), Box::new(right)), + => FollowAndCondition::Simple(simple), +}; + +FollowSimpleCondition: FollowSimpleCondition = { + => FollowSimpleCondition::Comparison(comp), + "(" ")" => FollowSimpleCondition::Nested(Box::new(cond)), +}; + +FollowComparison: FollowComparison = { + ">" => FollowComparison::GreaterThan(var, value), + "<" => FollowComparison::LessThan(var, value), + "==" => FollowComparison::EqualTo(var, value), +}; + +FollowVariable: FollowVariable = { + "F" => FollowVariable::F, + "G" => FollowVariable::G, +}; + +FollowAction: FollowAction = { + "{" "," "}" => FollowAction { expr_f, expr_g }, +}; + +FollowExpr: FollowExpr = { + => FollowExpr::Value(value), + => FollowExpr::Variable(var), +}; + Comma: Vec = { ",")*> => match e {