diff --git a/integration_tests/format_test.ucg b/integration_tests/format_test.ucg index edb7020..03e15c1 100644 --- a/integration_tests/format_test.ucg +++ b/integration_tests/format_test.ucg @@ -1,16 +1,36 @@ -assert { - ok = "hello @" % ("world") == "hello world", - desc = "\"hello @\" % (\"world\") == \"hello world\"", +let t = import "std/testing.ucg".asserts{}; + +assert t.equal{ + left = "hello @" % ("world"), + right = "hello world", }; -assert { - ok = "1 @ @" % (2, 3) == "1 2 3", - desc = "\"1 @ @\" % (2, 3) == \"1 2 3\"", + +assert t.equal{ + left = "1 @ @" % (2, 3), + right = "1 2 3", }; -assert { - ok = "@ or @" % (true, false) == "true or false", - desc = "\"@ or @\" % (true, false) == \"true or false\"", + +assert t.equal{ + left = "@ or @" % (true, false), + right = "true or false", }; -assert { - ok = "@" % (NULL) == "NULL", - desc = "\"@\" % (NULL) == \"NULL\"", + +assert t.equal{ + left = "@" % (NULL), + right = "NULL", +}; + +assert t.equal{ + left = "bar is just great" % {foo="bar"}, + right = "bar is just great", +}; + +assert t.equal{ + left = "@{item.foo} is just great" % {foo="bar"}, + right = "bar is just great", +}; + +assert t.equal{ + left = "@{{foo=item.foo}.foo} is just great" % {foo="bar"}, + right = "bar is just great", }; \ No newline at end of file diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 469454f..ccc0c62 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -459,11 +459,18 @@ pub struct CopyDef { pub pos: Position, } +/// Encodes one of two possible forms for format expression arguments. +#[derive(Debug, PartialEq, Clone)] +pub enum FormatArgs { + List(Vec), + Single(Box), +} + /// Encodes a format expression in the UCG AST. #[derive(Debug, PartialEq, Clone)] pub struct FormatDef { pub template: String, - pub args: Vec, + pub args: FormatArgs, pub pos: Position, } diff --git a/src/ast/walk.rs b/src/ast/walk.rs index ae1438c..1a00ca2 100644 --- a/src/ast/walk.rs +++ b/src/ast/walk.rs @@ -65,11 +65,16 @@ impl<'a> AstWalker<'a> { Expression::Copy(ref mut def) => { self.walk_fieldset(&mut def.fields); } - Expression::Format(ref mut def) => { - for expr in def.args.iter_mut() { + Expression::Format(ref mut def) => match def.args { + FormatArgs::List(ref mut args) => { + for expr in args.iter_mut() { + self.walk_expression(expr); + } + } + FormatArgs::Single(ref mut expr) => { self.walk_expression(expr); } - } + }, Expression::FuncOp(ref mut def) => match def { FuncOpDef::Reduce(ref mut def) => { self.walk_expression(def.target.as_mut()); diff --git a/src/build/mod.rs b/src/build/mod.rs index 3c8988b..d404379 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -33,7 +33,7 @@ use crate::ast::*; use crate::build::scope::{find_in_fieldlist, Scope, ValueMap}; use crate::convert::ImporterRegistry; use crate::error; -use crate::format; +use crate::format::{ExpressionFormatter, FormatRenderer, SimpleFormatter}; use crate::iter::OffsetStrIter; use crate::parse::parse; @@ -908,8 +908,7 @@ impl<'a> FileBuilder<'a> { return Ok(Rc::new(Val::Boolean(false))); } else { // Handle our tuple case since this isn't a list. - let mut child_scope = scope.spawn_child(); - child_scope.set_curr_val(right.clone()); + let child_scope = scope.spawn_child().set_curr_val(right.clone()); // Search for the field in our tuple or list. let maybe_val = self.do_dot_lookup(left, &child_scope); // Return the result of the search. @@ -969,8 +968,7 @@ impl<'a> FileBuilder<'a> { } } let left = self.eval_expr(&def.left, scope)?; - let mut child_scope = scope.spawn_child(); - child_scope.set_curr_val(left.clone()); + let child_scope = scope.spawn_child().set_curr_val(left.clone()); if let &BinaryExprType::DOT = kind { return self.do_dot_lookup(&def.right, &child_scope); }; @@ -1093,8 +1091,7 @@ impl<'a> FileBuilder<'a> { fn eval_copy(&self, def: &CopyDef, scope: &Scope) -> Result, Box> { let v = self.eval_value(&def.selector, scope)?; if let &Val::Tuple(ref src_fields) = v.as_ref() { - let mut child_scope = scope.spawn_child(); - child_scope.set_curr_val(v.clone()); + let child_scope = scope.spawn_child().set_curr_val(v.clone()); return self.copy_from_base(&src_fields, &def.fields, &child_scope); } if let &Val::Module(ref mod_def) = v.as_ref() { @@ -1108,8 +1105,7 @@ impl<'a> FileBuilder<'a> { // argset. // Push our base tuple on the stack so the copy can use // self to reference it. - let mut child_scope = scope.spawn_child(); - child_scope.set_curr_val(maybe_tpl.clone()); + let child_scope = scope.spawn_child().set_curr_val(maybe_tpl.clone()); let mod_args = self.copy_from_base(src_fields, &def.fields, &child_scope)?; // put our copied parameters tuple in our builder under the mod key. let mod_key = @@ -1156,14 +1152,27 @@ impl<'a> FileBuilder<'a> { fn eval_format(&self, def: &FormatDef, scope: &Scope) -> Result, Box> { let tmpl = &def.template; - let args = &def.args; - let mut vals = Vec::new(); - for v in args.iter() { - let rcv = self.eval_expr(v, scope)?; - vals.push(rcv.deref().clone()); - } - let formatter = format::Formatter::new(tmpl.clone(), vals); - Ok(Rc::new(Val::Str(formatter.render(&def.pos)?))) + return match &def.args { + FormatArgs::List(ref args) => { + let mut vals = Vec::new(); + for v in args.iter() { + let rcv = self.eval_expr(v, scope)?; + vals.push(rcv.deref().clone()); + } + let formatter = SimpleFormatter::new(tmpl.clone(), vals); + Ok(Rc::new(Val::Str(formatter.render(&def.pos)?))) + } + FormatArgs::Single(ref expr) => { + let val = self.eval_expr(expr, scope)?; + let mut builder = self.clone_builder(); + builder.scope.build_output.insert( + PositionedItem::new("item".to_string(), expr.pos().clone()), + val, + ); + let formatter = ExpressionFormatter::new(tmpl.clone(), builder); + Ok(Rc::new(Val::Str(formatter.render(&def.pos)?))) + } + }; } fn eval_call(&self, def: &CallDef, scope: &Scope) -> Result, Box> { diff --git a/src/build/scope.rs b/src/build/scope.rs index 76cf31c..9d2b0e9 100644 --- a/src/build/scope.rs +++ b/src/build/scope.rs @@ -97,8 +97,9 @@ impl Scope { } /// Set the current value for our execution context. - pub fn set_curr_val(&mut self, val: Rc) { + pub fn set_curr_val(mut self, val: Rc) -> Self { self.curr_val = Some(val); + self } /// Lookup up a list index in the current value diff --git a/src/format.rs b/src/format.rs index 1d4110b..ec7b06c 100644 --- a/src/format.rs +++ b/src/format.rs @@ -13,32 +13,41 @@ // limitations under the License. //! The format string logic for ucg format expressions. +use std::cell::RefCell; use std::clone::Clone; use std::error::Error; +use std::str::Chars; use crate::ast::*; +use crate::build::{FileBuilder, Val}; use crate::error; +pub trait FormatRenderer { + fn render(&self, pos: &Position) -> Result>; +} + /// Implements the logic for format strings in UCG format expressions. -pub struct Formatter + Clone> { +pub struct SimpleFormatter + Clone> { tmpl: String, args: Vec, } -impl + Clone> Formatter { +impl + Clone> SimpleFormatter { /// Constructs a Formatter with a template and args. pub fn new>(tmpl: S, args: Vec) -> Self { - Formatter { + SimpleFormatter { tmpl: tmpl.into(), args: args, } } +} +impl + Clone> FormatRenderer for SimpleFormatter { /// Renders a formatter to a string or returns an error. /// /// If the formatter has the wrong number of arguments for the number of replacements /// it will return an error. Otherwise it will return the formatted string. - pub fn render(&self, pos: &Position) -> Result> { + fn render(&self, pos: &Position) -> Result> { let mut buf = String::new(); let mut should_escape = false; let mut count = 0; @@ -74,28 +83,141 @@ impl + Clone> Formatter { } } +pub struct ExpressionFormatter<'a> { + tmpl: String, + builder: RefCell>, +} + +impl<'a> ExpressionFormatter<'a> { + pub fn new>(tmpl: S, builder: FileBuilder<'a>) -> Self { + ExpressionFormatter { + tmpl: tmpl.into(), + builder: RefCell::new(builder), + } + } + + fn consume_expr( + &self, + builder: &mut FileBuilder, + iter: &mut Chars, + pos: &Position, + ) -> Result> { + // we expect the next char to be { or we error. + // TODO(jwall): Consume until you reach the last '}' + let mut expr_string = String::new(); + let mut brace_count = 0; + match iter.next() { + Some(c) => { + if c == '{' { + brace_count += 1; + } else { + return Err(Box::new(error::BuildError::new( + format!( + "Invalid syntax for format string expected '{{' but got {}", + c + ), + error::ErrorType::FormatError, + pos.clone(), + ))); + } + } + None => { + return Err(Box::new(error::BuildError::new( + "Invalid syntax for format string expected '{' but string ended", + error::ErrorType::FormatError, + pos.clone(), + ))); + } + }; + loop { + let c = match iter.next() { + Some(c) => c, + None => break, + }; + if c == '{' { + brace_count += 1; + } + if c == '}' { + brace_count -= 1; + // if brace_count is 0 then this is the end of expression. + if brace_count != 0 { + // if it is not zero then this character is just part of + // the embedded expression. + expr_string.push(c); + continue; + } + // empty expressions are an error + if expr_string.is_empty() { + return Err(Box::new(error::BuildError::new( + "Got an empty expression in format string", + error::ErrorType::FormatError, + pos.clone(), + ))); + } + if !expr_string.ends_with(";") { + expr_string.push(';'); + } + // we are done and it is time to compute the expression and return it. + return Ok(builder.eval_string(&expr_string)?.as_ref().clone()); + } else { + expr_string.push(c); + } + } + return Err(Box::new(error::BuildError::new( + "Expected '}' but got end of string", + error::ErrorType::FormatError, + pos.clone(), + ))); + } +} + +impl<'a> FormatRenderer for ExpressionFormatter<'a> { + fn render(&self, pos: &Position) -> Result> { + let mut buf = String::new(); + let mut should_escape = false; + let mut iter = self.tmpl.chars(); + loop { + let c = match iter.next() { + Some(c) => c, + None => break, + }; + if c == '@' && !should_escape { + // This is kind of wasteful. Can we do better? + let val = self.consume_expr(&mut self.builder.borrow_mut(), &mut iter, pos)?; + let strval: String = val.into(); + buf.push_str(&strval); + } else if c == '\\' && !should_escape { + should_escape = true; + } else { + buf.push(c); + } + } + return Ok(buf); + } +} + #[cfg(test)] mod test { - use super::Formatter; + use super::{FormatRenderer, SimpleFormatter}; use crate::ast::Position; #[test] fn test_format_happy_path() { - let formatter = Formatter::new("foo @ @ \\@", vec!["bar", "quux"]); + let formatter = SimpleFormatter::new("foo @ @ \\@", vec!["bar", "quux"]); let pos = Position::new(0, 0, 0); assert_eq!(formatter.render(&pos).unwrap(), "foo bar quux @"); } #[test] fn test_format_happy_wrong_too_few_args() { - let formatter = Formatter::new("foo @ @ \\@", vec!["bar"]); + let formatter = SimpleFormatter::new("foo @ @ \\@", vec!["bar"]); let pos = Position::new(0, 0, 0); assert!(formatter.render(&pos).is_err()); } #[test] fn test_format_happy_wrong_too_many_args() { - let formatter = Formatter::new("foo @ @ \\@", vec!["bar", "quux", "baz"]); + let formatter = SimpleFormatter::new("foo @ @ \\@", vec!["bar", "quux", "baz"]); let pos = Position::new(0, 0, 0); assert!(formatter.render(&pos).is_err()); } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 262295c..08986b0 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -480,23 +480,35 @@ fn select_expression(input: SliceIter) -> Result, Expres } } -fn tuple_to_format(tok: Token, exprs: Vec) -> Expression { - Expression::Format(FormatDef { - template: tok.fragment.to_string(), - args: exprs, - pos: tok.pos, - }) -} +make_fn!( + simple_format_args, FormatArgs>, + do_each!( + _ => punct!("("), + args => separated!(punct!(","), trace_parse!(expression)), + _ => must!(punct!(")")), + (FormatArgs::List(args)) + ) +); + +make_fn!( + expression_format_args, FormatArgs>, + do_each!( + expr => must!(expression), + (FormatArgs::Single(Box::new(expr))) + ) +); make_fn!( format_expression, Expression>, do_each!( tmpl => match_type!(STR), _ => punct!("%"), - _ => must!(punct!("(")), - args => separated!(punct!(","), trace_parse!(expression)), - _ => must!(punct!(")")), - (tuple_to_format(tmpl, args)) + args => either!(simple_format_args, expression_format_args), + (Expression::Format(FormatDef { + template: tmpl.fragment.to_string(), + args: args, + pos: tmpl.pos, + })) ) );