FEATURE: Add the range expression.

fixes: #24
This commit is contained in:
Jeremy Wall 2019-01-10 18:38:14 -06:00
parent d2a5a1619a
commit 6f9ba2ac33
10 changed files with 200 additions and 3 deletions

View File

@ -223,6 +223,16 @@ should be placed. Any primitive value can be used as an argument.
"https://@:@/" % (host, port)
```
Range Expression
----------------
UCG can generate lists from a range with an optional step.
```
1:10 == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
0:2:10 == [0, 2, 4, 6, 8, 10];
```
Functional processing expressions
---------------------------------

View File

@ -141,6 +141,12 @@ reduce_expr: reduce_keyword, bareword, dot, bareword, expr, expr ;
processing_expr: map_or_filter_expr | reduce_expr
```
### Range Expression
```
range_expr: expr, ':', [int, ':'], expr ;
```
#### Include Expression
```
@ -155,6 +161,7 @@ non_operator_expr: literal
| macrodef
| module_def
| format_expr
| range_expr
| include_expr
| copy_expr
| processing_expr

View File

@ -4,10 +4,25 @@ let concatenated = ["foo"] + ["bar"];
assert {
ok = concatenated == ["foo", "bar"],
desc = "concatenated == [\"foo\", \"bar\"]",
desc = "Successfully concatenated",
};
assert {
ok = concatenated + empty_list == ["foo", "bar"],
desc = "concatenated + empty_list == [\"foo\", \"bar\"]",
desc = "successfully concatenated empty list",
};
assert {
ok = 1:5 == [1, 2, 3, 4, 5],
desc = "expected list of 5 but got @" % (1:5),
};
assert {
ok = 0:2:6 == [0, 2, 4, 6],
desc = "Expected evens up to 6 but got @" % (0:2:6),
};
assert {
ok = 0:(1+3) == [0, 1, 2, 3, 4],
desc = "Expected 0 through 4 but got @" % (0:(1+3)),
};

View File

@ -61,4 +61,10 @@ let mymodule = module { foo = "bar" } => {
assert {
ok = mymodule{}.foo == "bar",
desc = "mymodule{}.foo == \"bar\"",
};
let idx = 1;
assert {
ok = list.(idx) == 2,
desc = "expected 2, got @" % (list.(idx)),
};

View File

@ -456,6 +456,7 @@ impl MacroDef {
&Expression::Macro(_)
| &Expression::Copy(_)
| &Expression::Module(_)
| &Expression::Range(_)
| &Expression::FuncOp(_)
| &Expression::Include(_) => {
// noop
@ -633,6 +634,15 @@ impl ModuleDef {
}
}
/// RangeDef defines a range with optional step.
#[derive(Debug, PartialEq, Clone)]
pub struct RangeDef {
pub pos: Position,
pub start: Box<Expression>,
pub step: Option<Box<Expression>>,
pub end: Box<Expression>,
}
/// Encodes a ucg expression. Expressions compute a value from.
#[derive(Debug, PartialEq, Clone)]
pub enum Expression {
@ -644,6 +654,7 @@ pub enum Expression {
// Complex Expressions
Copy(CopyDef),
Range(RangeDef),
// TODO(jwall): This should really store it's position :-(
Grouped(Box<Expression>),
Format(FormatDef),
@ -662,6 +673,7 @@ impl Expression {
&Expression::Simple(ref v) => v.pos(),
&Expression::Binary(ref def) => &def.pos,
&Expression::Copy(ref def) => &def.pos,
&Expression::Range(ref def) => &def.pos,
&Expression::Grouped(ref expr) => expr.pos(),
&Expression::Format(ref def) => &def.pos,
&Expression::Call(ref def) => &def.pos,
@ -689,6 +701,9 @@ impl fmt::Display for Expression {
&Expression::Copy(_) => {
write!(w, "<Copy>")?;
}
&Expression::Range(_) => {
write!(w, "<Range>")?;
}
&Expression::Grouped(_) => {
write!(w, "(<Expr>)")?;
}

View File

@ -768,7 +768,24 @@ impl<'a> FileBuilder<'a> {
Expression::Simple(Value::Int(ref i)) => {
scope.lookup_idx(right.pos(), &Val::Int(i.val))
}
_ => self.eval_expr(right, scope),
_ => {
let val = self.eval_expr(right, scope)?;
match val.as_ref() {
Val::Int(i) => scope.lookup_idx(right.pos(), &Val::Int(*i)),
Val::Str(ref s) => scope
.lookup_sym(&PositionedItem::new(s.clone(), pos.clone()), false)
.ok_or(Box::new(error::BuildError::new(
format!("Unable to find binding {}", s,),
error::ErrorType::NoSuchSymbol,
pos,
))),
_ => Err(Box::new(error::BuildError::new(
format!("Invalid selector lookup {}", val.type_name(),),
error::ErrorType::NoSuchSymbol,
pos,
))),
}
}
}
}
@ -1518,6 +1535,65 @@ impl<'a> FileBuilder<'a> {
}
}
pub fn eval_range(&self, def: &RangeDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
let start = self.eval_expr(&def.start, scope)?;
let start = match start.as_ref() {
&Val::Int(i) => i,
_ => {
return Err(Box::new(error::BuildError::new(
format!(
"Expected an integer for range start but got {}",
start.type_name()
),
error::ErrorType::TypeFail,
def.start.pos().clone(),
)));
}
};
// See if there was a step.
let step = match &def.step {
Some(step) => {
let step = self.eval_expr(&step, scope)?;
match step.as_ref() {
&Val::Int(i) => i,
_ => {
return Err(Box::new(error::BuildError::new(
format!(
"Expected an integer for range step but got {}",
step.type_name()
),
error::ErrorType::TypeFail,
def.start.pos().clone(),
)));
}
}
}
None => 1,
};
// Get the end.
let end = self.eval_expr(&def.end, scope)?;
let end = match end.as_ref() {
&Val::Int(i) => i,
_ => {
return Err(Box::new(error::BuildError::new(
format!(
"Expected an integer for range start but got {}",
end.type_name()
),
error::ErrorType::TypeFail,
def.start.pos().clone(),
)));
}
};
let vec = (start..end + 1)
.step_by(step as usize)
.map(|i| Rc::new(Val::Int(i)))
.collect();
Ok(Rc::new(Val::List(vec)))
}
// Evals a single Expression in the context of a running Builder.
// It does not mutate the builders collected state at all.
pub fn eval_expr(&self, expr: &Expression, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
@ -1525,6 +1601,7 @@ impl<'a> FileBuilder<'a> {
&Expression::Simple(ref val) => self.eval_value(val, scope),
&Expression::Binary(ref def) => self.eval_binary(def, scope),
&Expression::Copy(ref def) => self.eval_copy(def, scope),
&Expression::Range(ref def) => self.eval_range(def, scope),
&Expression::Grouped(ref expr) => self.eval_expr(expr, scope),
&Expression::Format(ref def) => self.eval_format(def, scope),
&Expression::Call(ref def) => self.eval_call(def, scope),

View File

@ -631,11 +631,35 @@ make_fn!(
either!(reduce_expression, map_expression, filter_expression)
);
make_fn!(
range_expression<SliceIter<Token>, Expression>,
do_each!(
pos => pos,
start => either!(simple_expression, grouped_expression),
_ => punct!(":"),
maybe_step => optional!(
do_each!(
step => either!(simple_expression, grouped_expression),
_ => punct!(":"),
(Box::new(step))
)
),
end => either!(simple_expression, grouped_expression),
(Expression::Range(RangeDef{
pos: pos,
start: Box::new(start),
step: maybe_step,
end: Box::new(end),
}))
)
);
fn unprefixed_expression(input: SliceIter<Token>) -> ParseResult<Expression> {
let _input = input.clone();
either!(
input,
trace_parse!(format_expression),
trace_parse!(range_expression),
trace_parse!(simple_expression),
trace_parse!(call_expression),
trace_parse!(copy_expression)

View File

@ -242,6 +242,10 @@ make_fn!(semicolontok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::PUNCT, ";")
);
make_fn!(colontok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::PUNCT, ":")
);
make_fn!(leftsquarebracket<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::PUNCT, "[")
);
@ -389,6 +393,7 @@ fn token<'a>(input: OffsetStrIter<'a>) -> Result<OffsetStrIter<'a>, Token> {
fatcommatok, // Note fatcommatok must come before equaltok
equaltok,
semicolontok,
colontok,
leftsquarebracket,
rightsquarebracket,
booleantok,

View File

@ -49,4 +49,37 @@ let enumerate = module{
let enumerated = reduce reducer.result acc, (mod.list);
let result = enumerated.list;
};
let zip = module{
list1 = NULL,
list2 = NULL,
} => {
let counter = macro(acc, item) => {
result = acc + 1,
};
let len1 = reduce counter.result 0, (mod.list1);
let len2 = reduce counter.result 0, (mod.list2);
let rng = select (len1 >= len2), NULL, {
true = 0:(len1 - 1),
false = 0:(len2 - 1),
};
let reducer = macro(acc, item) => {
result = acc{
result = acc.result + [[acc.list1.(item), acc.list2.(item)]],
idxs = acc.idxs + [item]
},
};
let acc = {
list1 = mod.list1,
list2 = mod.list2,
result = [],
idxs = [],
};
let result = (reduce reducer.result acc, rng).result;
};

View File

@ -37,4 +37,9 @@ assert asserts.equal{
list=["foo", "bar"]
}.result,
right=[[1, "foo"], [3, "bar"]],
};
assert asserts.equal{
left=list.zip{list1=[0, 1], list2=[3, 4]}.result,
right=[[0, 3], [1, 4]],
};