From b9def2f15c08a65668e764601e91a6e6c10a70a3 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 18 Jan 2022 09:14:05 -0500 Subject: [PATCH] Add all replacement filter functionality --- mps-interpreter/README.md | 23 +- mps-interpreter/src/lang/filter.rs | 269 ++++++++++++---- mps-interpreter/src/lang/filter_replace.rs | 292 ++++++++++++++++++ mps-interpreter/src/lang/mod.rs | 2 + mps-interpreter/src/lang/utility.rs | 10 +- mps-interpreter/src/lang/vocabulary/repeat.rs | 6 +- mps-interpreter/src/lib.rs | 23 +- mps-interpreter/src/processing/variables.rs | 14 +- mps-interpreter/src/tokens/token_enum.rs | 10 + mps-interpreter/src/tokens/tokenizer.rs | 2 +- mps-interpreter/tests/single_line.rs | 21 +- src/help.rs | 6 +- 12 files changed, 588 insertions(+), 90 deletions(-) create mode 100644 mps-interpreter/src/lang/filter_replace.rs diff --git a/mps-interpreter/README.md b/mps-interpreter/README.md index f1520e0..74f72ff 100644 --- a/mps-interpreter/README.md +++ b/mps-interpreter/README.md @@ -51,26 +51,37 @@ field != something field >= something field > something field <= something -field < something -- e.g. iterable.(title == "Romantic Traffic"); +field < something -- e.g. `iterable.(title == "Romantic Traffic");` + Compare all items, keeping only those that match the condition. Valid field names are those of the MpsMusicItem (title, artist, album, genre, track, etc.), though this will change when proper object support is added. Optionally, a ? or ! can be added to the end of the field name to skip items whose field is missing/incomparable, or keep all items whose field is missing/incomparable (respectively). -start..end -- e.g. iterable.(0..42); +start..end -- e.g. `iterable.(0..42);` + Keep only the items that are at the start index up to the end index. Start and/or end may be omitted to start/stop at the iterable's existing start/end (respectively). This stops once the end condition is met, leaving the rest of the iterator unconsumed. -start..=end -- e.g. iterable.(0..=42); +start..=end -- e.g. `iterable.(0..=42);` + Keep only the items that are at the start index up to and including the end index. Start may be omitted to start at the iterable's existing start. This stops once the end condition is met, leaving the rest of the iterator unconsumed. -index -- e.g. iterable.(4); +index -- e.g. `iterable.(4);` + Keep only the item at the given index. This stops once the index is reached, leaving the rest of the iterator unconsumed. -predicate1 || predicate2 -- e.g. iterable.(4 || 5); +predicate1 || predicate2 -- e.g. `iterable.(4 || 5);` + Keep only the items that meet the criteria of predicate1 or predicate2. This will always consume the full iterator. -[empty] -- e.g. iterable.(); +[empty] -- e.g. `iterable.();` + Matches all items +if filter: operation1 else operation2 -- e.g. `iterable.(if title == "Romantic Traffic": repeat(item, 2) else item.());` + + Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`. + ### Functions Similar to most other languages: `function_name(param1, param2, etc.);`. +These always return an iterable which can me manipulated. Functions are statements of the format `function_name(params)`, where "function_name" is one of the function names (below) and params is a valid parameter input for the function. Each function is responsible for parsing it's own parameters when the statement is parsed, so this is very flexible. E.g. `files(folder="~/Music/", recursive=true);` is valid function syntax to execute the files function with parameters `folder="~/Music/", recursive=true`. diff --git a/mps-interpreter/src/lang/filter.rs b/mps-interpreter/src/lang/filter.rs index 1d39329..93c1cf5 100644 --- a/mps-interpreter/src/lang/filter.rs +++ b/mps-interpreter/src/lang/filter.rs @@ -3,11 +3,12 @@ use std::fmt::{Debug, Display, Error, Formatter}; use std::iter::Iterator; use std::marker::PhantomData; -use crate::lang::utility::{assert_token, assert_token_raw}; +use crate::lang::utility::{assert_token, assert_token_raw, check_name, assert_name}; use crate::lang::MpsLanguageDictionary; use crate::lang::{BoxedMpsOpFactory, MpsOp, PseudoOp}; use crate::lang::{RuntimeError, SyntaxError}; use crate::lang::SingleItem; +use crate::lang::MpsFilterReplaceStatement; use crate::processing::general::MpsType; use crate::processing::OpGetter; use crate::tokens::MpsToken; @@ -40,7 +41,7 @@ pub trait MpsFilterFactory { } #[derive(Debug, Clone)] -enum VariableOrOp { +pub(super) enum VariableOrOp { Variable(String), Op(PseudoOp), } @@ -75,7 +76,11 @@ impl std::clone::Clone for MpsFilterStatement

Display for MpsFilterStatement

{ fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "{}.({})", self.iterable, self.predicate) + if let Some(other_filters) = &self.other_filters { + write!(f, "{}.({} || (like) {})", self.iterable, self.predicate, other_filters) + } else { + write!(f, "{}.({})", self.iterable, self.predicate) + } } } @@ -89,18 +94,25 @@ impl MpsOp for MpsFilterStatement

{ } fn is_resetable(&self) -> bool { - match &self.iterable { + let is_iterable_resetable = match &self.iterable { VariableOrOp::Variable(s) => { - let var = self.context.as_ref().unwrap().variables.get_opt(s); - if let Some(MpsType::Op(var)) = var { - var.is_resetable() - } else { - false - } + if self.context.is_some() { + let var = self.context.as_ref().unwrap().variables.get_opt(s); + if let Some(MpsType::Op(var)) = var { + var.is_resetable() + } else { + false + } + } else {true} // ASSUMPTION + } VariableOrOp::Op(PseudoOp::Real(op)) => op.is_resetable(), VariableOrOp::Op(PseudoOp::Fake(_)) => false, - } + }; + let is_other_filter_resetable = if let Some(PseudoOp::Real(other_filter)) = &self.other_filters { + other_filter.is_resetable() + } else {true}; + is_iterable_resetable && is_other_filter_resetable } fn reset(&mut self) -> Result<(), RuntimeError> { @@ -108,30 +120,43 @@ impl MpsOp for MpsFilterStatement

{ self.predicate.reset()?; match &mut self.iterable { VariableOrOp::Variable(s) => { - let fake_getter = &mut move || fake.clone(); - if let MpsType::Op(var) = self - .context - .as_mut() - .unwrap() - .variables - .get_mut(s, fake_getter)? - { - var.reset() - } else { - Err(RuntimeError { - line: 0, - op: PseudoOp::Fake(format!("{}", self)), - msg: "Cannot reset non-iterable filter variable".to_string(), - }) - } - } - VariableOrOp::Op(PseudoOp::Real(op)) => op.reset(), + if self.context.as_mut().unwrap().variables.exists(s) { + let fake_getter = &mut move || fake.clone(); + let mut var = self.context.as_mut().unwrap().variables.remove(s, fake_getter)?; + let result = if let MpsType::Op(var) = &mut var { + var.enter(self.context.take().unwrap()); + let result = var.reset(); + self.context = Some(var.escape()); + result + } else { + Err(RuntimeError { + line: 0, + op: fake_getter(), + msg: "Cannot reset non-iterable filter variable".to_string(), + }) + }; + self.context.as_mut().unwrap().variables.declare(s, var, fake_getter)?; + result + } else {Ok(())} + }, + VariableOrOp::Op(PseudoOp::Real(op)) => { + op.enter(self.context.take().unwrap()); + let result = op.reset(); + self.context = Some(op.escape()); + result + }, VariableOrOp::Op(PseudoOp::Fake(_)) => Err(RuntimeError { line: 0, op: fake, msg: "Cannot reset PseudoOp::Fake filter".to_string(), }), - } + }?; + if let Some(PseudoOp::Real(other_filter)) = &mut self.other_filters { + other_filter.enter(self.context.take().unwrap()); + let result = other_filter.reset(); + self.context = Some(other_filter.escape()); + result + } else {Ok(())} } } @@ -338,14 +363,30 @@ impl + 'static> BoxedMps if start_of_predicate > tokens_len - 1 { false } else { - if let Some(pipe_location) = first_double_pipe(tokens) { + let pipe_location_opt = last_double_pipe(tokens, 1); + if pipe_location_opt.is_some() && pipe_location_opt.unwrap() > start_of_predicate { + let pipe_location = pipe_location_opt.unwrap(); + // filters combined by OR operations let tokens2: VecDeque<&MpsToken> = VecDeque::from_iter(tokens.range(start_of_predicate..pipe_location)); self.filter_factory.is_filter(&tokens2) } else { + // single filter let tokens2: VecDeque<&MpsToken> = VecDeque::from_iter(tokens.range(start_of_predicate..tokens_len - 1)); - self.filter_factory.is_filter(&tokens2) + if tokens2.len() != 0 && check_name("if", &tokens2[0]) { + // replacement filter + if let Some(colon_location) = first_colon2(&tokens2) { + let tokens3 = VecDeque::from_iter(tokens.range(start_of_predicate+1..start_of_predicate+colon_location)); + self.filter_factory.is_filter(&tokens3) + } else { + false + } + } else { + // regular filter + self.filter_factory.is_filter(&tokens2) + } + } } @@ -382,40 +423,95 @@ impl + 'static> BoxedMps } assert_token_raw(MpsToken::Dot, tokens)?; assert_token_raw(MpsToken::OpenBracket, tokens)?; - let mut another_filter = None; - let (has_or, end_tokens) = if let Some(pipe_location) = first_double_pipe(tokens) { - (true, tokens.split_off(pipe_location)) // parse up to OR operator + if !tokens.is_empty() && check_name("if", &tokens[0]) { + return { + // replacement filter + //println!("Building replacement filter from tokens {:?}", tokens); + assert_name("if", tokens)?; + if let Some(colon_location) = first_colon(tokens) { + let end_tokens = tokens.split_off(colon_location); + let filter = self.filter_factory.build_filter(tokens, dict)?; + tokens.extend(end_tokens); + assert_token_raw(MpsToken::Colon, tokens)?; + let mut else_op: Option = None; + let if_op: PseudoOp; + if let Some(else_location) = first_else_not_in_bracket(tokens) { + let end_tokens = tokens.split_off(else_location); + // build replacement system + if_op = dict.try_build_statement(tokens)?.into(); + tokens.extend(end_tokens); + assert_name("else", tokens)?; + let end_tokens = tokens.split_off(tokens.len() - 1); // up to ending close bracket + // build replacement system + else_op = Some(dict.try_build_statement(tokens)?.into()); + tokens.extend(end_tokens); + } else { + let end_tokens = tokens.split_off(tokens.len() - 1); + // build replacement system + if_op = dict.try_build_statement(tokens)?.into(); + tokens.extend(end_tokens); + } + assert_token_raw(MpsToken::CloseBracket, tokens)?; + Ok(Box::new(MpsFilterReplaceStatement { + predicate: filter, + iterable: op, + context: None, + op_if: if_op, + op_else: else_op, + item_cache: super::filter_replace::item_cache_deque() + })) + } else { + Err(SyntaxError { + line: 0, + token: MpsToken::Colon, + got: None, + }) + } + } } else { - (false, tokens.split_off(tokens.len()-1)) // don't parse closing bracket in filter - }; - let filter = self.filter_factory.build_filter(tokens, dict)?; - tokens.extend(end_tokens); - if has_or { - // recursively build other filters for OR operation - assert_token_raw(MpsToken::Pipe, tokens)?; - assert_token_raw(MpsToken::Pipe, tokens)?; - // emit fake filter syntax - tokens.push_front(MpsToken::OpenBracket); - tokens.push_front(MpsToken::Dot); - tokens.push_front(MpsToken::Name(INNER_VARIABLE_NAME.into())); // impossible to obtain through parsing on purpose - another_filter = Some(dict.try_build_statement(tokens)?.into()); - } else { - assert_token_raw(MpsToken::CloseBracket, tokens)?; // remove closing bracket + let mut another_filter = None; + let (has_or, end_tokens) = if let Some(pipe_location) = last_double_pipe(tokens, 1) { + (true, tokens.split_off(pipe_location)) // parse up to OR operator + } else { + (false, tokens.split_off(tokens.len()-1)) // don't parse closing bracket in filter + }; + let filter = self.filter_factory.build_filter(tokens, dict)?; + tokens.extend(end_tokens); + if has_or { + // recursively build other filters for OR operation + assert_token_raw(MpsToken::Pipe, tokens)?; + assert_token_raw(MpsToken::Pipe, tokens)?; + // emit fake filter syntax + tokens.push_front(MpsToken::OpenBracket); + tokens.push_front(MpsToken::Dot); + tokens.push_front(MpsToken::Name(INNER_VARIABLE_NAME.into())); // impossible to obtain through parsing on purpose + another_filter = Some(dict.try_build_statement(tokens)?.into()); + } else { + assert_token_raw(MpsToken::CloseBracket, tokens)?; // remove closing bracket + } + Ok(Box::new(MpsFilterStatement { + predicate: filter, + iterable: op, + context: None, + other_filters: another_filter, + })) } - Ok(Box::new(MpsFilterStatement { - predicate: filter, - iterable: op, - context: None, - other_filters: another_filter, - })) + } } fn last_open_bracket_is_after_dot(tokens: &VecDeque) -> bool { + let mut inside_brackets = 0; let mut open_bracket_found = false; for i in (0..tokens.len()).rev() { - if tokens[i].is_open_bracket() { - open_bracket_found = true; + if tokens[i].is_close_bracket() { + inside_brackets += 1; + } else if tokens[i].is_open_bracket() { + if inside_brackets == 1 { + open_bracket_found = true; + } else if inside_brackets != 0 { + inside_brackets -= 1; + } } else if open_bracket_found { if tokens[i].is_dot() { return true; @@ -428,10 +524,17 @@ fn last_open_bracket_is_after_dot(tokens: &VecDeque) -> bool { } fn last_dot_before_open_bracket(tokens: &VecDeque) -> usize { + let mut inside_brackets = 0; let mut open_bracket_found = false; for i in (0..tokens.len()).rev() { - if tokens[i].is_open_bracket() { - open_bracket_found = true; + if tokens[i].is_close_bracket() { + inside_brackets += 1; + } else if tokens[i].is_open_bracket() { + if inside_brackets == 1 { + open_bracket_found = true; + } else if inside_brackets != 0 { + inside_brackets -= 1; + } } else if open_bracket_found { if tokens[i].is_dot() { return i; @@ -443,17 +546,55 @@ fn last_dot_before_open_bracket(tokens: &VecDeque) -> usize { 0 } -fn first_double_pipe(tokens: &VecDeque) -> Option { +fn last_double_pipe(tokens: &VecDeque, in_brackets: usize) -> Option { + let mut inside_brackets = 0; let mut pipe_found = false; - for i in 0..tokens.len() { - if tokens[i].is_pipe() { + for i in (0..tokens.len()).rev() { + if tokens[i].is_pipe() && inside_brackets == in_brackets { if pipe_found { - return Some(i-1); + return Some(i); } else { pipe_found = true; } } else { pipe_found = false; + if tokens[i].is_close_bracket() { + inside_brackets += 1; + } else if tokens[i].is_open_bracket() && inside_brackets != 0 { + inside_brackets -= 1; + } + } + } + None +} + +fn first_colon(tokens: &VecDeque) -> Option { + for i in 0..tokens.len() { + if tokens[i].is_colon() { + return Some(i); + } + } + None +} + +fn first_colon2(tokens: &VecDeque<&MpsToken>) -> Option { + for i in 0..tokens.len() { + if tokens[i].is_colon() { + return Some(i); + } + } + None +} + +fn first_else_not_in_bracket(tokens: &VecDeque) -> Option { + let mut inside_brackets = 0; + for i in 0..tokens.len() { + if check_name("else", &tokens[i]) && inside_brackets == 0 { + return Some(i); + } else if tokens[i].is_open_bracket() { + inside_brackets += 1; + } else if tokens[i].is_close_bracket() && inside_brackets != 0 { + inside_brackets -= 1; } } None diff --git a/mps-interpreter/src/lang/filter_replace.rs b/mps-interpreter/src/lang/filter_replace.rs new file mode 100644 index 0000000..474186c --- /dev/null +++ b/mps-interpreter/src/lang/filter_replace.rs @@ -0,0 +1,292 @@ +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::iter::Iterator; + +use crate::lang::{MpsOp, PseudoOp}; +use crate::lang::RuntimeError; +use crate::lang::{MpsFilterPredicate, filter::VariableOrOp}; +use crate::lang::SingleItem; +use crate::processing::general::MpsType; +use crate::processing::OpGetter; +use crate::MpsContext; +use crate::MpsMusicItem; + +const ITEM_VARIABLE_NAME: &str = "item"; +const ITEM_CACHE_DEFAULT_SIZE: usize = 8; + +#[inline(always)] +pub(super) fn item_cache_deque() -> VecDeque> { + VecDeque::with_capacity(ITEM_CACHE_DEFAULT_SIZE) +} + +#[derive(Debug)] +pub struct MpsFilterReplaceStatement { + pub(super) predicate: P, + pub(super) iterable: VariableOrOp, + pub(super) context: Option, + pub(super) op_if: PseudoOp, + pub(super) op_else: Option, + pub(super) item_cache: VecDeque>, +} + +impl std::clone::Clone for MpsFilterReplaceStatement

{ + fn clone(&self) -> Self { + Self { + predicate: self.predicate.clone(), + iterable: self.iterable.clone(), + context: None, + op_if: self.op_if.clone(), + op_else: self.op_else.clone(), + item_cache: VecDeque::new(), // this doesn't need to be carried around + } + } +} + +impl Display for MpsFilterReplaceStatement

{ + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + if let Some(op_else) = &self.op_else { + write!(f, "{}.(if {}: {} else {})", self.iterable, self.predicate, self.op_if, op_else) + } else { + write!(f, "{}.(if {}: {})", self.iterable, self.predicate, self.op_if) + } + } +} + +impl MpsOp for MpsFilterReplaceStatement

{ + fn enter(&mut self, ctx: MpsContext) { + self.context = Some(ctx) + } + + fn escape(&mut self) -> MpsContext { + self.context.take().unwrap() + } + + fn is_resetable(&self) -> bool { + match &self.iterable { + VariableOrOp::Variable(s) => { + if self.context.is_some() { + let var = self.context.as_ref().unwrap().variables.get_opt(s); + if let Some(MpsType::Op(var)) = var { + var.is_resetable() + } else { + false + } + } else {true} // ASSUMPTION + + } + VariableOrOp::Op(PseudoOp::Real(op)) => op.is_resetable(), + VariableOrOp::Op(PseudoOp::Fake(_)) => false, + } + } + + fn reset(&mut self) -> Result<(), RuntimeError> { + self.item_cache.clear(); + let fake = PseudoOp::Fake(format!("{}", self)); + self.predicate.reset()?; + match &mut self.iterable { + VariableOrOp::Variable(s) => { + if self.context.as_mut().unwrap().variables.exists(s) { + let fake_getter = &mut move || fake.clone(); + let mut var = self.context.as_mut().unwrap().variables.remove(s, fake_getter)?; + let result = if let MpsType::Op(var) = &mut var { + var.enter(self.context.take().unwrap()); + let result = var.reset(); + self.context = Some(var.escape()); + result + } else { + Err(RuntimeError { + line: 0, + op: fake_getter(), + msg: "Cannot reset non-iterable filter variable".to_string(), + }) + }; + self.context.as_mut().unwrap().variables.declare(s, var, fake_getter)?; + result + } else {Ok(())} + }, + VariableOrOp::Op(PseudoOp::Real(op)) => { + op.enter(self.context.take().unwrap()); + let result = op.reset(); + self.context = Some(op.escape()); + result + }, + VariableOrOp::Op(PseudoOp::Fake(_)) => Err(RuntimeError { + line: 0, + op: fake, + msg: "Cannot reset PseudoOp::Fake filter".to_string(), + }), + } + } +} + +impl Iterator for MpsFilterReplaceStatement

{ + type Item = Result; + + fn next(&mut self) -> Option { + if !self.item_cache.is_empty() { + return self.item_cache.pop_front(); + } + let self_clone = self.clone(); + let mut op_getter = move || (Box::new(self_clone.clone()) as Box).into(); + // get next item in iterator + let next_item = match &mut self.iterable { + VariableOrOp::Op(op) => match op.try_real() { + Ok(real_op) => { + let ctx = self.context.take().unwrap(); + real_op.enter(ctx); + let item = real_op.next(); + self.context = Some(real_op.escape()); + item + } + Err(e) => return Some(Err(e)), + }, + VariableOrOp::Variable(variable_name) => { + let mut variable = match self + .context + .as_mut() + .unwrap() + .variables + .remove(&variable_name, &mut op_getter) + { + Ok(MpsType::Op(op)) => op, + Ok(x) => { + return Some(Err(RuntimeError { + line: 0, + op: op_getter(), + msg: format!( + "Expected operation/iterable type in variable {}, got {}", + &variable_name, x + ), + })) + } + Err(e) => return Some(Err(e)), + }; + let ctx = self.context.take().unwrap(); + variable.enter(ctx); + let item = variable.next(); + self.context = Some(variable.escape()); + match self.context.as_mut().unwrap().variables.declare( + &variable_name, + MpsType::Op(variable), + &mut op_getter, + ) { + Err(e) => return Some(Err(e)), + Ok(_) => {}, + } + item + } + }; + // process item + match next_item { + Some(Ok(item)) => { + //println!("item is now: `{}`", &item.filename); + match self.predicate.matches(&item, self.context.as_mut().unwrap(), &mut op_getter) { + Ok(is_match) => + if is_match { + // unwrap inner operation + match self.op_if.try_real() { + Ok(real_op) => { + // build item variable + let single_op = SingleItem::new_ok(item); + //println!("Declaring item variable"); + let old_item = match declare_or_replace_item(single_op, &mut op_getter, self.context.as_mut().unwrap()) { + Ok(x) => x, + Err(e) => return Some(Err(e)), // probably shouldn't occur + }; + // invoke inner op + real_op.enter(self.context.take().unwrap()); + if real_op.is_resetable() { + match real_op.reset() { + Err(e) => return Some(Err(e)), + Ok(_) => {} + } + } + while let Some(item) = real_op.next() { + self.item_cache.push_back(item); + } + self.context = Some(real_op.escape()); + // destroy item variable + //println!("Removing item variable"); + match remove_or_replace_item(old_item, &mut op_getter, self.context.as_mut().unwrap()) { + Ok(_) => {}, + Err(e) => return Some(Err(e)) + } + } + Err(e) => return Some(Err(e)), // probably shouldn't occur + } + // return cached item, if any + let replacement = self.item_cache.pop_front(); + if replacement.is_none() { + self.next() + } else { + replacement + } + } else if let Some(op_else) = &mut self.op_else { + // unwrap inner operation + match op_else.try_real() { + Ok(real_op) => { + // build item variable + let single_op = SingleItem::new_ok(item); + //println!("Declaring item variable"); + let old_item = match declare_or_replace_item(single_op, &mut op_getter, self.context.as_mut().unwrap()) { + Ok(x) => x, + Err(e) => return Some(Err(e)), // probably shouldn't occur + }; + // invoke inner operation + real_op.enter(self.context.take().unwrap()); + if real_op.is_resetable() { + match real_op.reset() { + Err(e) => return Some(Err(e)), + Ok(_) => {} + } + } + while let Some(item) = real_op.next() { + self.item_cache.push_back(item); + } + self.context = Some(real_op.escape()); + // destroy item variable + //println!("Removing item variable"); + match remove_or_replace_item(old_item, &mut op_getter, self.context.as_mut().unwrap()) { + Ok(_) => {}, + Err(e) => return Some(Err(e)) + } + } + Err(e) => return Some(Err(e)), // probably shouldn't occur + } + // return cached item, if any + let replacement = self.item_cache.pop_front(); + if replacement.is_none() { + self.next() + } else { + replacement + } + } else { + Some(Ok(item)) + }, + Err(e) => return Some(Err(e)) + } + }, + Some(Err(e)) => Some(Err(e)), + None => None, + } + } +} + +fn declare_or_replace_item(single: SingleItem, op: &mut OpGetter, ctx: &mut MpsContext) -> Result, RuntimeError> { + let old_item: Option; + if ctx.variables.exists(ITEM_VARIABLE_NAME) { + old_item = Some(ctx.variables.remove(ITEM_VARIABLE_NAME, op)?); + } else { + old_item = None; + } + ctx.variables.declare(ITEM_VARIABLE_NAME, MpsType::Op(Box::new(single)), op)?; + Ok(old_item) +} + +fn remove_or_replace_item(old_item: Option, op: &mut OpGetter, ctx: &mut MpsContext) -> Result<(), RuntimeError> { + ctx.variables.remove(ITEM_VARIABLE_NAME, op)?; + if let Some(old_item) = old_item { + ctx.variables.declare(ITEM_VARIABLE_NAME, old_item, op)?; + } + Ok(()) +} diff --git a/mps-interpreter/src/lang/mod.rs b/mps-interpreter/src/lang/mod.rs index 3a0ef71..d042337 100644 --- a/mps-interpreter/src/lang/mod.rs +++ b/mps-interpreter/src/lang/mod.rs @@ -2,6 +2,7 @@ mod db_items; mod dictionary; mod error; mod filter; +mod filter_replace; mod function; mod lookup; mod operation; @@ -17,6 +18,7 @@ pub use error::{MpsLanguageError, RuntimeError, SyntaxError}; pub use filter::{ MpsFilterFactory, MpsFilterPredicate, MpsFilterStatement, MpsFilterStatementFactory, }; +pub use filter_replace::MpsFilterReplaceStatement; pub use function::{MpsFunctionFactory, MpsFunctionStatementFactory}; pub use lookup::Lookup; pub use operation::{BoxedMpsOpFactory, MpsOp, MpsOpFactory, SimpleMpsOpFactory}; diff --git a/mps-interpreter/src/lang/utility.rs b/mps-interpreter/src/lang/utility.rs index 03a1310..8980449 100644 --- a/mps-interpreter/src/lang/utility.rs +++ b/mps-interpreter/src/lang/utility.rs @@ -121,9 +121,9 @@ pub fn check_is_type(token: &MpsToken) -> bool { match token { MpsToken::Literal(_) => true, MpsToken::Name(s) => { - s.parse::().is_ok() - || s.parse::().is_ok() + s.parse::().is_ok() || s.parse::().is_ok() + || s.parse::().is_ok() || s == "false" || s == "true" } @@ -143,12 +143,12 @@ pub fn assert_type(tokens: &mut VecDeque) -> Result Ok(MpsTypePrimitive::String(s)), MpsToken::Name(s) => { - if let Ok(f) = s.parse::() { - Ok(MpsTypePrimitive::Float(f)) - } else if let Ok(i) = s.parse::() { + if let Ok(i) = s.parse::() { Ok(MpsTypePrimitive::Int(i)) } else if let Ok(u) = s.parse::() { Ok(MpsTypePrimitive::UInt(u)) + } else if let Ok(f) = s.parse::() { + Ok(MpsTypePrimitive::Float(f)) } else if s == "false" { Ok(MpsTypePrimitive::Bool(false)) } else if s == "true" { diff --git a/mps-interpreter/src/lang/vocabulary/repeat.rs b/mps-interpreter/src/lang/vocabulary/repeat.rs index 444880f..65d1ca9 100644 --- a/mps-interpreter/src/lang/vocabulary/repeat.rs +++ b/mps-interpreter/src/lang/vocabulary/repeat.rs @@ -25,7 +25,11 @@ pub struct RepeatStatement { impl Display for RepeatStatement { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "repeat({})", self.inner_statement) + if self.loop_forever { + write!(f, "repeat({})", self.inner_statement) + } else { + write!(f, "repeat({}, {})", self.inner_statement, self.original_repetitions) + } } } diff --git a/mps-interpreter/src/lib.rs b/mps-interpreter/src/lib.rs index a6c9e12..2292d06 100644 --- a/mps-interpreter/src/lib.rs +++ b/mps-interpreter/src/lib.rs @@ -49,26 +49,37 @@ //! field >= something //! field > something //! field <= something -//! field < something -- e.g. iterable.(title == "Romantic Traffic"); +//! field < something -- e.g. `iterable.(title == "Romantic Traffic");` +//! //! Compare all items, keeping only those that match the condition. Valid field names are those of the MpsMusicItem (title, artist, album, genre, track, etc.), though this will change when proper object support is added. Optionally, a ? or ! can be added to the end of the field name to skip items whose field is missing/incomparable, or keep all items whose field is missing/incomparable (respectively). //! -//! start..end -- e.g. iterable.(0..42); +//! start..end -- e.g. `iterable.(0..42);` +//! //! Keep only the items that are at the start index up to the end index. Start and/or end may be omitted to start/stop at the iterable's existing start/end (respectively). This stops once the end condition is met, leaving the rest of the iterator unconsumed. //! -//! start..=end -- e.g. iterable.(0..=42); +//! start..=end -- e.g. `iterable.(0..=42);` +//! //! Keep only the items that are at the start index up to and including the end index. Start may be omitted to start at the iterable's existing start. This stops once the end condition is met, leaving the rest of the iterator unconsumed. //! -//! index -- e.g. iterable.(4); +//! index -- e.g. `iterable.(4);` +//! //! Keep only the item at the given index. This stops once the index is reached, leaving the rest of the iterator unconsumed. //! -//! predicate1 || predicate2 -- e.g. iterable.(4 || 5); +//! predicate1 || predicate2 -- e.g. `iterable.(4 || 5);` +//! //! Keep only the items that meet the criteria of predicate1 or predicate2. This will always consume the full iterator. //! -//! [empty] -- e.g. iterable.(); +//! [empty] -- e.g. `iterable.();` +//! //! Matches all items //! +//! if filter: operation1 else operation2 -- e.g. `iterable.(if title == "Romantic Traffic": repeat(item, 2) else item.());` +//! +//! Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`. +//! //! ## Functions //! Similar to most other languages: `function_name(param1, param2, etc.);`. +//! These always return an iterable which can me manipulated. //! Functions are statements of the format `function_name(params)`, where "function_name" is one of the function names (below) and params is a valid parameter input for the function. //! Each function is responsible for parsing it's own parameters when the statement is parsed, so this is very flexible. //! E.g. `files(folder="~/Music/", recursive=true);` is valid function syntax to execute the files function with parameters `folder="~/Music/", recursive=true`. diff --git a/mps-interpreter/src/processing/variables.rs b/mps-interpreter/src/processing/variables.rs index c02321f..fe1ee7b 100644 --- a/mps-interpreter/src/processing/variables.rs +++ b/mps-interpreter/src/processing/variables.rs @@ -30,7 +30,7 @@ pub trait MpsVariableStorer: Debug { None => Err(RuntimeError { line: 0, op: op(), - msg: format!("Variable {} not found", name), + msg: format!("Variable '{}' not found", name), }), } } @@ -43,7 +43,7 @@ pub trait MpsVariableStorer: Debug { None => Err(RuntimeError { line: 0, op: op(), - msg: format!("Variable {} not found", name), + msg: format!("Variable '{}' not found", name), }), } } @@ -61,6 +61,10 @@ pub trait MpsVariableStorer: Debug { ) -> Result<(), RuntimeError>; fn remove(&mut self, name: &str, op: &mut OpGetter) -> Result; + + fn exists(&self, name: &str) -> bool { + self.get_opt(name).is_some() + } } #[derive(Default, Debug)] @@ -82,7 +86,7 @@ impl MpsVariableStorer for MpsOpStorage { Err(RuntimeError { line: 0, op: op(), - msg: format!("Cannot assign to non-existent variable {}", key), + msg: format!("Cannot assign to non-existent variable '{}'", key), }) } else { self.storage.insert(key.to_string(), item); @@ -95,7 +99,7 @@ impl MpsVariableStorer for MpsOpStorage { Err(RuntimeError { line: 0, op: op(), - msg: format!("Cannot overwrite existing variable {}", key), + msg: format!("Cannot overwrite existing variable '{}'", key), }) } else { self.storage.insert(key.to_string(), item); @@ -110,7 +114,7 @@ impl MpsVariableStorer for MpsOpStorage { Err(RuntimeError { line: 0, op: op(), - msg: format!("Cannot remove non-existing variable {}", key), + msg: format!("Cannot remove non-existing variable '{}'", key), }) } } diff --git a/mps-interpreter/src/tokens/token_enum.rs b/mps-interpreter/src/tokens/token_enum.rs index 61cb9a4..cdd4ba0 100644 --- a/mps-interpreter/src/tokens/token_enum.rs +++ b/mps-interpreter/src/tokens/token_enum.rs @@ -19,6 +19,7 @@ pub enum MpsToken { Interrogation, Pipe, Ampersand, + Colon, } impl MpsToken { @@ -38,6 +39,7 @@ impl MpsToken { "?" => Ok(Self::Interrogation), "|" => Ok(Self::Pipe), "&" => Ok(Self::Ampersand), + ":" => Ok(Self::Colon), _ => { // name validation let mut ok = true; @@ -174,6 +176,13 @@ impl MpsToken { _ => false, } } + + pub fn is_colon(&self) -> bool { + match self { + Self::Colon => true, + _ => false, + } + } } impl Display for MpsToken { @@ -196,6 +205,7 @@ impl Display for MpsToken { Self::Interrogation => write!(f, "?"), Self::Pipe => write!(f, "|"), Self::Ampersand => write!(f, "&"), + Self::Colon => write!(f, ":") } } } diff --git a/mps-interpreter/src/tokens/tokenizer.rs b/mps-interpreter/src/tokens/tokenizer.rs index d159f44..eee3f40 100644 --- a/mps-interpreter/src/tokens/tokenizer.rs +++ b/mps-interpreter/src/tokens/tokenizer.rs @@ -261,7 +261,7 @@ impl ReaderStateMachine { '\n'| '\r' | '\t' | ' ' => Self::EndToken {}, ';' => Self::EndStatement {}, '\0' => Self::EndOfFile {}, - '(' | ')' | ',' | '=' | '<' | '>' | '.' | '!' | '?' | '|' => Self::SingleCharToken { out: input }, + '(' | ')' | ',' | '=' | '<' | '>' | '.' | '!' | '?' | '|' | ':' => Self::SingleCharToken { out: input }, _ => Self::Regular { out: input }, }, Self::Escaped { inside } => match inside { diff --git a/mps-interpreter/tests/single_line.rs b/mps-interpreter/tests/single_line.rs index 030b1cc..64ec59c 100644 --- a/mps-interpreter/tests/single_line.rs +++ b/mps-interpreter/tests/single_line.rs @@ -132,7 +132,7 @@ fn execute_assign_line() -> Result<(), Box> { #[test] fn execute_emptyfilter_line() -> Result<(), Box> { - execute_single_line("song(`lov`).()", false, true) + execute_single_line("files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).().().()", false, true) } #[test] @@ -264,3 +264,22 @@ fn execute_orfilter_line() -> Result<(), Box> { true, ) } + +#[test] +fn execute_replacefilter_line() -> Result<(), Box> { + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(if 4: files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(5))", + false, + true, + )?; + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(if 4: files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(5) else item.())", + false, + true, + )?; + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(if 4: item.() else files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(0 || 1).(if 200: files() else repeat(item.(), 2)))", + false, + true, + ) +} diff --git a/src/help.rs b/src/help.rs index a4e9253..69103a7 100644 --- a/src/help.rs +++ b/src/help.rs @@ -7,6 +7,7 @@ To view the currently-supported operations, try ?functions and ?filters"; pub const FUNCTIONS: &str = "FUNCTIONS (?functions) Similar to most other languages: function_name(param1, param2, etc.) +These always return an iterable which can me manipulated. sql_init(generate = true|false, folder = `path/to/music`) Initialize the SQLite database connection using the provided parameters. This must be performed before any other database operation (otherwise the database will already be connected with default settings). @@ -57,4 +58,7 @@ Operations to reduce the items in an iterable: iterable.(filter) Keep only the items that meet the criteria of filter1 or filter2. This will always consume the full iterator. [empty] -- e.g. iterable.() - Matches all items"; + Matches all items + + if filter: operation1 else operation2 -- e.g. iterable.(if title == `Romantic Traffic`: repeat(item, 2) else item.()) + Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`.";