diff --git a/docsite/site/content/reference/expressions.md b/docsite/site/content/reference/expressions.md index de251fc..ef43d80 100644 --- a/docsite/site/content/reference/expressions.md +++ b/docsite/site/content/reference/expressions.md @@ -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 --------------------------------- diff --git a/docsite/site/content/reference/grammar.md b/docsite/site/content/reference/grammar.md index a86b66a..2fdda06 100644 --- a/docsite/site/content/reference/grammar.md +++ b/docsite/site/content/reference/grammar.md @@ -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 diff --git a/integration_tests/list_test.ucg b/integration_tests/list_test.ucg index 122a5c9..18e3f3a 100644 --- a/integration_tests/list_test.ucg +++ b/integration_tests/list_test.ucg @@ -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)), }; \ No newline at end of file diff --git a/integration_tests/selectors_test.ucg b/integration_tests/selectors_test.ucg index b936d73..efaea24 100644 --- a/integration_tests/selectors_test.ucg +++ b/integration_tests/selectors_test.ucg @@ -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)), }; \ No newline at end of file diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 91038f1..67b9e2d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -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, + pub step: Option>, + pub end: Box, +} + /// 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), 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, "")?; } + &Expression::Range(_) => { + write!(w, "")?; + } &Expression::Grouped(_) => { write!(w, "()")?; } diff --git a/src/build/mod.rs b/src/build/mod.rs index 7f0fdab..19fdff7 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -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, Box> { + 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, Box> { @@ -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), diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 312e01c..33822ed 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -631,11 +631,35 @@ make_fn!( either!(reduce_expression, map_expression, filter_expression) ); +make_fn!( + range_expression, 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) -> ParseResult { 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) diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs index 1f1e1e7..ccc6c3d 100644 --- a/src/tokenizer/mod.rs +++ b/src/tokenizer/mod.rs @@ -242,6 +242,10 @@ make_fn!(semicolontok, do_text_token_tok!(TokenType::PUNCT, ";") ); +make_fn!(colontok, + do_text_token_tok!(TokenType::PUNCT, ":") +); + make_fn!(leftsquarebracket, do_text_token_tok!(TokenType::PUNCT, "[") ); @@ -389,6 +393,7 @@ fn token<'a>(input: OffsetStrIter<'a>) -> Result, Token> { fatcommatok, // Note fatcommatok must come before equaltok equaltok, semicolontok, + colontok, leftsquarebracket, rightsquarebracket, booleantok, diff --git a/std/lists.ucg b/std/lists.ucg index 9a6a6b8..ea1fb91 100644 --- a/std/lists.ucg +++ b/std/lists.ucg @@ -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; }; \ No newline at end of file diff --git a/std/tests/lists_test.ucg b/std/tests/lists_test.ucg index 7185937..b982b3d 100644 --- a/std/tests/lists_test.ucg +++ b/std/tests/lists_test.ucg @@ -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]], }; \ No newline at end of file