diff --git a/docsite/site/content/reference/expressions.md b/docsite/site/content/reference/expressions.md index e897bb4..ab8cbdf 100644 --- a/docsite/site/content/reference/expressions.md +++ b/docsite/site/content/reference/expressions.md @@ -223,6 +223,86 @@ should be placed. Any primitive value can be used as an argument. "https://@:@/" % (host, port) ``` +Functional processing expressions +--------------------------------- + +UCG has a few functional processing expressions called `map` and `filter`. Both of +them can process a list or tuple. + +Their syntax starts with either map or filter followed by a symbol that +references a valid macro and the outfield for the tuple the macro produces. and +finally an expression that resolves to either a list of tuple. + +### Map expressions + +Map macros should produce in the result field a value or [field, value] that +will replace the element or field it is curently processing. + +**For Lists** + +When mapping a macro across a list the result field can be any valid value. The +macro is expected to take a single argument. + +``` +let list1 = [1, 2, 3, 4]; + +let mapper = macro(item) => { result = item + 1 }; +map mapper.result list1 == [2, 3, 4, 5]; +``` + +**For Tuples** + +Macros for mapping across a tuple are expected to take two arguments. The first +argument is the name of the field. The second argument is the value in that +field. The result field should be a two item list with the first item being the +new field name and the second item being the new value. + +``` +let test_tpl = { + foo = "bar", + quux = "baz", +}; +let tpl_mapper = macro(name, val) => { + result = select name, [name, val], { + "foo" = ["foo", "barbar"], + quux = ["cute", "pygmy"], + }, +}; +map tpl_mapper.result test_tpl == {foo = "barbar", cute = "pygmy"}; +``` + +### Filter expressions + +Filter expressions should return a result field with false or NULL for items to +filter out of the list or tuple. Any other value in the return field results in +the item or field staying in the resulting list or tuple. + +**Lists** + +``` +let list2 = ["foo", "bar", "foo", "bar"]; +let filtrator = macro(item) => { + result = select item, NULL, { + foo = item, + }, +}; + +filter filtrator.result list2 == ["foo", "foo"]; +``` + +**Tuples** + +``` +let test_tpl = { + foo = "bar", + quux = "baz", +}; +let tpl_filter = macro(name, val) => { + result = name != "foo", +}; +filter tpl_filter.result test_tpl == { quux = "baz" }; +``` + Include expressions ------------------- diff --git a/integration_tests/functional_processing_test.ucg b/integration_tests/functional_processing_test.ucg index b27ca7a..f5a3b0b 100644 --- a/integration_tests/functional_processing_test.ucg +++ b/integration_tests/functional_processing_test.ucg @@ -13,7 +13,7 @@ let boolfiltrator = macro(item) => { }; assert | -map mapper.result list1 == [2, 3, 4, 5]; + map mapper.result list1 == [2, 3, 4, 5]; |; assert | (map mapper.result [1, 2, 3, 4]) == [2, 3, 4, 5]; @@ -33,4 +33,46 @@ assert | |; assert | filter boolfiltrator.result [1, 2, 3, 4, 5, 6, 7] == [1, 2, 3, 4]; +|; + +// Tuple processing +let test_tpl = { + foo = "bar", + quux = "baz", +}; + +let identity_tpl_mapper = macro(name, val) => { + result = [name, val], +}; + +assert | + map identity_tpl_mapper.result test_tpl == test_tpl; +|; + +let tpl_mapper = macro(name, val) => { + result = select name, [name, val], { + "foo" = ["foo", "barbar"], + quux = ["cute", "pygmy"], + }, +}; + +assert | + map tpl_mapper.result test_tpl == {foo = "barbar", cute = "pygmy"}; +|; + +let identity_tpl_filter = macro(name, val) => { + result = true, +}; + +// strip out foo field +let tpl_filter = macro(name, val) => { + result = name != "foo", +}; + +assert | + filter identity_tpl_filter.result test_tpl == test_tpl; +|; + +assert | + filter tpl_filter.result test_tpl == { quux = "baz" }; |; \ No newline at end of file diff --git a/src/build/mod.rs b/src/build/mod.rs index 53375c6..3b8a3ae 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1113,7 +1113,7 @@ impl<'a> FileBuilder<'a> { } fn eval_functional_list_processing( - &mut self, + &self, elems: &Vec>, def: &MacroDef, outfield: &PositionedItem, @@ -1144,6 +1144,77 @@ impl<'a> FileBuilder<'a> { return Ok(Rc::new(Val::List(out))); } + fn eval_functional_tuple_processing( + &self, + fs: &Vec<(PositionedItem, Rc)>, + def: &MacroDef, + outfield: &PositionedItem, + typ: &ListOpType, + ) -> Result, Box> { + let mut out = Vec::new(); + for &(ref name, ref val) in fs { + let argvals = vec![Rc::new(Val::Str(name.val.clone())), val.clone()]; + let fields = def.eval(self.file.clone(), self, argvals)?; + if let Some(v) = find_in_fieldlist(&outfield.val, &fields) { + match typ { + ListOpType::Map => { + if let &Val::List(ref fs) = v.as_ref() { + if fs.len() == 2 { + // index 0 should be a string for the new field name. + // index 1 should be the val. + let new_name = if let &Val::Str(ref s) = fs[0].as_ref() { + s.clone() + } else { + return Err(Box::new(error::BuildError::new( + format!( + "map on tuple expects the first item out list to be a string but got size {}", + fs[0].type_name() + ), + error::ErrorType::TypeFail, + def.pos.clone(), + ))); + }; + out.push(( + PositionedItem::new(new_name, name.pos.clone()), + fs[1].clone(), + )); + } else { + return Err(Box::new(error::BuildError::new( + format!( + "map on a tuple field expects a list of size 2 as output but got size {}", + fs.len() + ), + error::ErrorType::TypeFail, + def.pos.clone(), + ))); + } + } else { + return Err(Box::new(error::BuildError::new( + format!( + "map on a tuple field expects a list as output but got {:?}", + v.type_name() + ), + error::ErrorType::TypeFail, + def.pos.clone(), + ))); + } + } + ListOpType::Filter => { + if let &Val::Empty = v.as_ref() { + // noop + continue; + } else if let &Val::Boolean(false) = v.as_ref() { + // noop + continue; + } + out.push((name.clone(), val.clone())); + } + } + } + } + Ok(Rc::new(Val::Tuple(out))) + } + fn eval_functional_processing( &mut self, def: &ListOpDef, @@ -1165,6 +1236,9 @@ impl<'a> FileBuilder<'a> { &Val::List(ref elems) => { self.eval_functional_list_processing(elems, macdef, &def.field, &def.typ) } + &Val::Tuple(ref fs) => { + self.eval_functional_tuple_processing(fs, macdef, &def.field, &def.typ) + } other => Err(Box::new(error::BuildError::new( format!("Expected List as target but got {:?}", other.type_name()), error::ErrorType::TypeFail,