Parsing the map operations.

This commit is contained in:
Jeremy Wall 2018-03-15 19:08:33 -05:00
parent 51edc6d15c
commit eeac1ba599
7 changed files with 245 additions and 21 deletions

View File

@ -1,18 +1,11 @@
# Major Planned Features # Major Planned Features
## Boolean operations ## Boolean operations and type
* equality (for everything) * equality (for everything)
* contains (for lists or strings) * contains (for lists or strings)
* less than or greater than (for numeric types) * less than or greater than (for numeric types)
## List processing
* Map over lists
* Filtering lists
* Flags could automatically expand a list of values into a list of flags.
* Joining a lists elements. (i.e. folds)
## Query Language (Experimental) ## Query Language (Experimental)
You should be able to ask the compiler to tell you any value or set of values in the You should be able to ask the compiler to tell you any value or set of values in the

View File

@ -15,7 +15,13 @@ let db_conn1 = mk_db_conn(dbhost1, 3306, dbname);
let db_conn2 = mk_db_conn(dbhost2, 3306, dbname); let db_conn2 = mk_db_conn(dbhost2, 3306, dbname);
// We have two database connections in a list // We have two database connections in a list
let db_conns = [db_conn1.conn_string, db_conn2.conn_string]; let db_conn_list = [db_conn1, db_conn2];
let connstr_mapper = macro (item) => {
str = item.conn_string
};
let db_conns = map connstr_mapper.str [db_conn1, db_conn2];
// Our server configuration. // Our server configuration.
let server_config = { let server_config = {

View File

@ -599,6 +599,10 @@ impl MacroDef {
// noop // noop
continue; continue;
} }
&Expression::ListOp(_) => {
// noop
continue;
}
} }
} }
} }
@ -651,6 +655,21 @@ pub struct ListDef {
pub pos: Position, pub pos: Position,
} }
#[derive(Debug, PartialEq, Clone)]
pub enum ListOpType {
Map,
Filter,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ListOpDef {
pub typ: ListOpType,
pub mac: SelectorDef,
pub field: String,
pub target: ListDef,
pub pos: Position,
}
/// Encodes a ucg expression. Expressions compute a value from. /// Encodes a ucg expression. Expressions compute a value from.
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum Expression { pub enum Expression {
@ -667,6 +686,7 @@ pub enum Expression {
Call(CallDef), Call(CallDef),
Macro(MacroDef), Macro(MacroDef),
Select(SelectDef), Select(SelectDef),
ListOp(ListOpDef),
} }
impl Expression { impl Expression {
@ -681,6 +701,7 @@ impl Expression {
&Expression::Call(ref def) => &def.pos, &Expression::Call(ref def) => &def.pos,
&Expression::Macro(ref def) => &def.pos, &Expression::Macro(ref def) => &def.pos,
&Expression::Select(ref def) => &def.pos, &Expression::Select(ref def) => &def.pos,
&Expression::ListOp(ref def) => &def.pos,
} }
} }
} }

View File

@ -493,12 +493,16 @@ impl Builder {
&Val::List(_) => { &Val::List(_) => {
stack.push_back(first.clone()); stack.push_back(first.clone());
} }
_ => { val => {
eprintln!("Not a tuple or list! {:?}", val)
// noop // noop
} }
} }
if let &Some(ref tail) = &sl.tail { if let &Some(ref tail) = &sl.tail {
if tail.len() == 0 {
return Ok(first);
}
let mut it = tail.iter().peekable(); let mut it = tail.iter().peekable();
loop { loop {
let vref = stack.pop_front().unwrap(); let vref = stack.pop_front().unwrap();
@ -839,6 +843,39 @@ impl Builder {
} }
} }
// FIXME(jwall): We still need to write unit tests for these.
fn eval_list_op(&self, def: &ListOpDef) -> Result<Rc<Val>, Box<Error>> {
let l = &def.target.elems;
let mac = &def.mac;
if let &Val::Macro(ref macdef) = try!(self.lookup_selector(&mac.sel)).as_ref() {
let mut out = Vec::new();
for expr in l.iter() {
let argvals = vec![try!(self.eval_expr(expr))];
let fields = try!(macdef.eval(argvals));
if let Some(v) = Self::find_in_fieldlist(&def.field, &fields) {
match def.typ {
ListOpType::Map => {
out.push(v.clone());
}
ListOpType::Filter => {
if let &Val::Empty = v.as_ref() {
// noop
continue;
}
out.push(v.clone());
}
}
}
}
return Ok(Rc::new(Val::List(out)));
}
return Err(Box::new(error::Error::new(
format!("Expected macro but got {:?}", mac),
error::ErrorType::TypeFail,
def.pos.clone(),
)));
}
// Evals a single Expression in the context of a running Builder. // Evals a single Expression in the context of a running Builder.
// It does not mutate the builders collected state at all. // It does not mutate the builders collected state at all.
pub fn eval_expr(&self, expr: &Expression) -> Result<Rc<Val>, Box<Error>> { pub fn eval_expr(&self, expr: &Expression) -> Result<Rc<Val>, Box<Error>> {
@ -853,6 +890,7 @@ impl Builder {
&Expression::Call(ref def) => self.eval_call(def), &Expression::Call(ref def) => self.eval_call(def),
&Expression::Macro(ref def) => self.eval_macro_def(def), &Expression::Macro(ref def) => self.eval_macro_def(def),
&Expression::Select(ref def) => self.eval_select(def), &Expression::Select(ref def) => self.eval_select(def),
&Expression::ListOp(ref def) => self.eval_list_op(def),
} }
} }
} }

View File

@ -54,19 +54,25 @@
//! * select //! * select
//! * macro //! * macro
//! * env //! * env
//! * map
//! * filter
//! * NULL //! * NULL
//! //!
//! ### Primitive types //! ### Primitive types
//! //!
//! ucg has a relatively simple syntax with 3 primitive types, Int, Float, and String. //! ucg has a relatively simple syntax with 3 primitive types, Int, Float, and String.
//! //!
//! * An Int is any integer number. //! #### Int
//!
//! An Int is any integer number.
//! //!
//! ```ucg //! ```ucg
//! 1; // a single Integer //! 1; // a single Integer
//! ``` //! ```
//! //!
//! * A Float is any number with a decimal point. //! #### Float
//!
//! A Float is any number with a decimal point.
//! //!
//! ```ucg //! ```ucg
//! 1.0; // A typical float. //! 1.0; // A typical float.
@ -74,7 +80,9 @@
//! .1 // the leading 0 is also optional. //! .1 // the leading 0 is also optional.
//! ``` //! ```
//! //!
//! * A String is any quoted text. Backslashes within a string escape the next preceding //! #### String
//!
//! A String is any quoted text. Backslashes within a string escape the next preceding
//! character. //! character.
//! //!
//! ``` ucg //! ``` ucg
@ -82,7 +90,9 @@
//! "I'm a \"fine\" looking string"; // escaped quotes in a string. //! "I'm a \"fine\" looking string"; // escaped quotes in a string.
//! ``` //! ```
//! //!
//! * A NULL is an empty type. It represents no value. //! #### NULL or the Empty type.
//!
//! A NULL is an empty type. It represents no value.
//! //!
//! ```ucg //! ```ucg
//! let empty = NULL; //! let empty = NULL;
@ -92,7 +102,9 @@
//! //!
//! ucg has two complex data types, Lists and Tuples. //! ucg has two complex data types, Lists and Tuples.
//! //!
//! * Lists are surrounded with square brackets `[ ]` and have comma separated elements. //! #### Lists
//!
//! Lists are surrounded with square brackets `[ ]` and have comma separated elements.
//! //!
//! ```ucg //! ```ucg
//! [1, 2, 3]; // A simple list of numbers. //! [1, 2, 3]; // A simple list of numbers.
@ -108,7 +120,47 @@
//! let zero = mylist.0; //! let zero = mylist.0;
//! ``` //! ```
//! //!
//! * Tuple's are an ordered collection of name, value pairs. They are bounded by curly braces `{ }` //! ##### List macros
//!
//! ucg supports a couple of ways to use macros for mapping or filtering a list to a new list.
//!
//! A map expression starts with the map keyword followed by the name of a macro with exactly
//! one argument, a `.`, and the name of the output field for the macro. ucg will apply the macro
//! to each element of the list and then take the output field from the resulting tuple and add append
//! it to the resulting list. If the output field does not exist in the macro it will be a compile
//! error.
//!
//! ```ucg
//! let list = [1, 2, 3, 4];
//! let mapper = macro(item) => { result = item + 1 };
//!
//! // results in: [2, 3, 4, 5]
//! let mapped = map mapper.result list;
//! ```
//!
//
//! A filter expression starts with the filter keyword followed by the name of a macro with exactly
//! one argument, a `.`, and the name of the output field for the macro. The filter will apply the
//! macro to each element of the list and if the output field is a Value that is not NULL then the
//! list element is appended to the output list. If the output field returns a NULL Value then the
//! element is not appended to the output list. If the output field does not exist in the macro it
//! will be a compile error.
//!
//! ```ucg
//! let list = ["foo", "bar", "foo", "bar"];
//! let filtrator = macro(item) => {
//! ok = select item NULL {
//! foo = 1
//! }
//! };
//!
//! // results in: ["foo", "foo"]
//! let filtered = filter filtrator.ok list;
//! ```
//!
//! #### Tuple
//!
//! Tuple's are an ordered collection of name, value pairs. They are bounded by curly braces `{ }`
//! and contain name = value pairs separated by commas. Trailing commas are permitted. The name must //! and contain name = value pairs separated by commas. Trailing commas are permitted. The name must
//! be a bareword without quotes. //! be a bareword without quotes.
//! //!
@ -223,10 +275,10 @@
//! //!
//! Macros look like functions but they are resolved at compile time and configurations don't execute so they never appear in output. //! Macros look like functions but they are resolved at compile time and configurations don't execute so they never appear in output.
//! They are useful for constructing tuples of a certain shape or otherwise promoting data reuse. You define a macro with the `macro` //! They are useful for constructing tuples of a certain shape or otherwise promoting data reuse. You define a macro with the `macro`
//! keyword followed by the arguments in parentheses and then a tuple. //! keyword followed by the arguments in parentheses, a `=>`, and then a tuple.
//! //!
//! ```ucg //! ```ucg
//! let myfunc = macro (arg1, arg2) { //! let myfunc = macro (arg1, arg2) => {
//! host = arg1, //! host = arg1,
//! port = arg2, //! port = arg2,
//! connstr = "couchdb://@:@" % (arg1, arg2), //! connstr = "couchdb://@:@" % (arg1, arg2),
@ -278,6 +330,10 @@
//! //!
//! let mysqlconf = dbconfigs.mysql; //! let mysqlconf = dbconfigs.mysql;
//! ``` //! ```
// The following is necessary to allow the macros in tokenizer and parse modules
// to succeed.
#![recursion_limit = "128"]
#[macro_use] #[macro_use]
extern crate nom; extern crate nom;
#[macro_use] #[macro_use]

View File

@ -404,7 +404,7 @@ named!(macro_expression<TokenIter, Expression, ParseError>,
map_res!( map_res!(
do_parse!( do_parse!(
pos: pos >> pos: pos >>
start: word!("macro") >> word!("macro") >>
punct!("(") >> punct!("(") >>
arglist: arglist >> arglist: arglist >>
punct!(")") >> punct!(")") >>
@ -512,6 +512,105 @@ named!(call_expression<TokenIter, Expression, ParseError>,
) )
); );
fn symbol_or_list(input: TokenIter) -> NomResult<Value> {
let sym = do_parse!(input, sym: symbol >> (sym));
match sym {
IResult::Incomplete(i) => {
return IResult::Incomplete(i);
}
IResult::Error(_) => {
// TODO(jwall): Still missing some. But we need to avoid recursion
match list_value(input) {
IResult::Incomplete(i) => {
return IResult::Incomplete(i);
}
IResult::Error(e) => {
return IResult::Error(e);
}
IResult::Done(i, val) => {
return IResult::Done(i, val);
}
}
}
IResult::Done(rest, val) => {
return IResult::Done(rest, val);
}
}
}
fn tuple_to_list_op(tpl: (Position, Token, Value, Value)) -> ParseResult<Expression> {
let pos = tpl.0;
let t = if &tpl.1.fragment == "map" {
ListOpType::Map
} else if &tpl.1.fragment == "filter" {
ListOpType::Filter
} else {
return Err(ParseError {
description: format!(
"Expected one of 'map' or 'filter' but got '{}'",
tpl.1.fragment
),
pos: pos,
});
};
let macroname = tpl.2;
let list = tpl.3;
if let Value::Selector(mut def) = macroname {
// First of all we need to assert that this is a selector of at least
// two sections.
let fieldname: String = match &mut def.sel.tail {
&mut None => {
return Err(ParseError {
description: format!("Missing a result field for the macro"),
pos: pos,
});
}
&mut Some(ref mut tl) => {
if tl.len() < 1 {
return Err(ParseError {
description: format!("Missing a result field for the macro"),
pos: def.pos.clone(),
});
}
let fname = tl.pop();
fname.unwrap().fragment
}
};
if let Value::List(ldef) = list {
return Ok(Expression::ListOp(ListOpDef {
typ: t,
mac: def,
field: fieldname,
target: ldef,
pos: pos,
}));
}
// TODO(jwall): We should print a pretter message than debug formatting here.
return Err(ParseError {
pos: pos,
description: format!("Expected a list but got {:?}", list),
});
}
return Err(ParseError {
pos: pos,
description: format!("Expected a macro but got {:?}", macroname),
});
}
named!(list_op_expression<TokenIter, Expression, ParseError>,
map_res!(
do_parse!(
pos: pos >>
optype: alt!(word!("map") | word!("filter")) >>
macroname: selector_value >>
list: symbol_or_list >>
(pos, optype, macroname, list)
),
tuple_to_list_op
)
);
// NOTE(jwall): HERE THERE BE DRAGONS. The order for these matters // NOTE(jwall): HERE THERE BE DRAGONS. The order for these matters
// alot. We need to process alternatives in order of decreasing // alot. We need to process alternatives in order of decreasing
// specificity. Unfortunately this means we are required to go in a // specificity. Unfortunately this means we are required to go in a
@ -525,6 +624,7 @@ named!(call_expression<TokenIter, Expression, ParseError>,
named!(expression<TokenIter, Expression, ParseError>, named!(expression<TokenIter, Expression, ParseError>,
do_parse!( do_parse!(
expr: alt!( expr: alt!(
complete!(list_op_expression) |
complete!(add_expression) | complete!(add_expression) |
complete!(sub_expression) | complete!(sub_expression) |
complete!(mul_expression) | complete!(mul_expression) |
@ -625,7 +725,7 @@ pub fn parse(input: LocatedSpan<&str>) -> Result<Vec<Statement>, ParseError> {
IResult::Error(e) => { IResult::Error(e) => {
return Err(ParseError { return Err(ParseError {
description: format!( description: format!(
"Tokenization error: {:?} current token: {:?}", "Statement Parse error: {:?} current token: {:?}",
e, i_[0] e, i_[0]
), ),
pos: Position { pos: Position {

View File

@ -205,6 +205,14 @@ named!(astok( Span ) -> Token,
do_tag_tok!(TokenType::BAREWORD, "as") do_tag_tok!(TokenType::BAREWORD, "as")
); );
named!(maptok( Span ) -> Token,
do_tag_tok!(TokenType::BAREWORD, "map")
);
named!(filtertok( Span ) -> Token,
do_tag_tok!(TokenType::BAREWORD, "filter")
);
fn end_of_input(input: Span) -> nom::IResult<Span, Token> { fn end_of_input(input: Span) -> nom::IResult<Span, Token> {
match eof!(input,) { match eof!(input,) {
nom::IResult::Done(_, _) => { nom::IResult::Done(_, _) => {
@ -273,7 +281,6 @@ named!(token( Span ) -> Token,
alt!( alt!(
strtok | strtok |
emptytok | // This must come before the barewordtok emptytok | // This must come before the barewordtok
barewordtok |
digittok | digittok |
commatok | commatok |
rbracetok | rbracetok |
@ -297,6 +304,9 @@ named!(token( Span ) -> Token,
macrotok | macrotok |
importtok | importtok |
astok | astok |
maptok |
filtertok |
barewordtok |
whitespace | whitespace |
end_of_input) end_of_input)
); );