FEATURE: Expression format string support.

Experimental support for Issue #23
This commit is contained in:
Jeremy Wall 2019-01-28 21:22:23 -06:00
parent 44055c28e9
commit 3c1b3ce86a
7 changed files with 229 additions and 53 deletions

View File

@ -1,16 +1,36 @@
assert { let t = import "std/testing.ucg".asserts{};
ok = "hello @" % ("world") == "hello world",
desc = "\"hello @\" % (\"world\") == \"hello world\"", assert t.equal{
left = "hello @" % ("world"),
right = "hello world",
}; };
assert {
ok = "1 @ @" % (2, 3) == "1 2 3", assert t.equal{
desc = "\"1 @ @\" % (2, 3) == \"1 2 3\"", left = "1 @ @" % (2, 3),
right = "1 2 3",
}; };
assert {
ok = "@ or @" % (true, false) == "true or false", assert t.equal{
desc = "\"@ or @\" % (true, false) == \"true or false\"", left = "@ or @" % (true, false),
right = "true or false",
}; };
assert {
ok = "@" % (NULL) == "NULL", assert t.equal{
desc = "\"@\" % (NULL) == \"NULL\"", 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",
}; };

View File

@ -459,11 +459,18 @@ pub struct CopyDef {
pub pos: Position, pub pos: Position,
} }
/// Encodes one of two possible forms for format expression arguments.
#[derive(Debug, PartialEq, Clone)]
pub enum FormatArgs {
List(Vec<Expression>),
Single(Box<Expression>),
}
/// Encodes a format expression in the UCG AST. /// Encodes a format expression in the UCG AST.
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct FormatDef { pub struct FormatDef {
pub template: String, pub template: String,
pub args: Vec<Expression>, pub args: FormatArgs,
pub pos: Position, pub pos: Position,
} }

View File

@ -65,11 +65,16 @@ impl<'a> AstWalker<'a> {
Expression::Copy(ref mut def) => { Expression::Copy(ref mut def) => {
self.walk_fieldset(&mut def.fields); self.walk_fieldset(&mut def.fields);
} }
Expression::Format(ref mut def) => { Expression::Format(ref mut def) => match def.args {
for expr in def.args.iter_mut() { FormatArgs::List(ref mut args) => {
for expr in args.iter_mut() {
self.walk_expression(expr);
}
}
FormatArgs::Single(ref mut expr) => {
self.walk_expression(expr); self.walk_expression(expr);
} }
} },
Expression::FuncOp(ref mut def) => match def { Expression::FuncOp(ref mut def) => match def {
FuncOpDef::Reduce(ref mut def) => { FuncOpDef::Reduce(ref mut def) => {
self.walk_expression(def.target.as_mut()); self.walk_expression(def.target.as_mut());

View File

@ -33,7 +33,7 @@ use crate::ast::*;
use crate::build::scope::{find_in_fieldlist, Scope, ValueMap}; use crate::build::scope::{find_in_fieldlist, Scope, ValueMap};
use crate::convert::ImporterRegistry; use crate::convert::ImporterRegistry;
use crate::error; use crate::error;
use crate::format; use crate::format::{ExpressionFormatter, FormatRenderer, SimpleFormatter};
use crate::iter::OffsetStrIter; use crate::iter::OffsetStrIter;
use crate::parse::parse; use crate::parse::parse;
@ -908,8 +908,7 @@ impl<'a> FileBuilder<'a> {
return Ok(Rc::new(Val::Boolean(false))); return Ok(Rc::new(Val::Boolean(false)));
} else { } else {
// Handle our tuple case since this isn't a list. // Handle our tuple case since this isn't a list.
let mut child_scope = scope.spawn_child(); let child_scope = scope.spawn_child().set_curr_val(right.clone());
child_scope.set_curr_val(right.clone());
// Search for the field in our tuple or list. // Search for the field in our tuple or list.
let maybe_val = self.do_dot_lookup(left, &child_scope); let maybe_val = self.do_dot_lookup(left, &child_scope);
// Return the result of the search. // Return the result of the search.
@ -969,8 +968,7 @@ impl<'a> FileBuilder<'a> {
} }
} }
let left = self.eval_expr(&def.left, scope)?; let left = self.eval_expr(&def.left, scope)?;
let mut child_scope = scope.spawn_child(); let child_scope = scope.spawn_child().set_curr_val(left.clone());
child_scope.set_curr_val(left.clone());
if let &BinaryExprType::DOT = kind { if let &BinaryExprType::DOT = kind {
return self.do_dot_lookup(&def.right, &child_scope); 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<Rc<Val>, Box<dyn Error>> { fn eval_copy(&self, def: &CopyDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
let v = self.eval_value(&def.selector, scope)?; let v = self.eval_value(&def.selector, scope)?;
if let &Val::Tuple(ref src_fields) = v.as_ref() { if let &Val::Tuple(ref src_fields) = v.as_ref() {
let mut child_scope = scope.spawn_child(); let child_scope = scope.spawn_child().set_curr_val(v.clone());
child_scope.set_curr_val(v.clone());
return self.copy_from_base(&src_fields, &def.fields, &child_scope); return self.copy_from_base(&src_fields, &def.fields, &child_scope);
} }
if let &Val::Module(ref mod_def) = v.as_ref() { if let &Val::Module(ref mod_def) = v.as_ref() {
@ -1108,8 +1105,7 @@ impl<'a> FileBuilder<'a> {
// argset. // argset.
// Push our base tuple on the stack so the copy can use // Push our base tuple on the stack so the copy can use
// self to reference it. // self to reference it.
let mut child_scope = scope.spawn_child(); let child_scope = scope.spawn_child().set_curr_val(maybe_tpl.clone());
child_scope.set_curr_val(maybe_tpl.clone());
let mod_args = self.copy_from_base(src_fields, &def.fields, &child_scope)?; 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. // put our copied parameters tuple in our builder under the mod key.
let mod_key = let mod_key =
@ -1156,14 +1152,27 @@ impl<'a> FileBuilder<'a> {
fn eval_format(&self, def: &FormatDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> { fn eval_format(&self, def: &FormatDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
let tmpl = &def.template; let tmpl = &def.template;
let args = &def.args; return match &def.args {
let mut vals = Vec::new(); FormatArgs::List(ref args) => {
for v in args.iter() { let mut vals = Vec::new();
let rcv = self.eval_expr(v, scope)?; for v in args.iter() {
vals.push(rcv.deref().clone()); 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)?))) 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<Rc<Val>, Box<dyn Error>> { fn eval_call(&self, def: &CallDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {

View File

@ -97,8 +97,9 @@ impl Scope {
} }
/// Set the current value for our execution context. /// Set the current value for our execution context.
pub fn set_curr_val(&mut self, val: Rc<Val>) { pub fn set_curr_val(mut self, val: Rc<Val>) -> Self {
self.curr_val = Some(val); self.curr_val = Some(val);
self
} }
/// Lookup up a list index in the current value /// Lookup up a list index in the current value

View File

@ -13,32 +13,41 @@
// limitations under the License. // limitations under the License.
//! The format string logic for ucg format expressions. //! The format string logic for ucg format expressions.
use std::cell::RefCell;
use std::clone::Clone; use std::clone::Clone;
use std::error::Error; use std::error::Error;
use std::str::Chars;
use crate::ast::*; use crate::ast::*;
use crate::build::{FileBuilder, Val};
use crate::error; use crate::error;
pub trait FormatRenderer {
fn render(&self, pos: &Position) -> Result<String, Box<dyn Error>>;
}
/// Implements the logic for format strings in UCG format expressions. /// Implements the logic for format strings in UCG format expressions.
pub struct Formatter<V: Into<String> + Clone> { pub struct SimpleFormatter<V: Into<String> + Clone> {
tmpl: String, tmpl: String,
args: Vec<V>, args: Vec<V>,
} }
impl<V: Into<String> + Clone> Formatter<V> { impl<V: Into<String> + Clone> SimpleFormatter<V> {
/// Constructs a Formatter with a template and args. /// Constructs a Formatter with a template and args.
pub fn new<S: Into<String>>(tmpl: S, args: Vec<V>) -> Self { pub fn new<S: Into<String>>(tmpl: S, args: Vec<V>) -> Self {
Formatter { SimpleFormatter {
tmpl: tmpl.into(), tmpl: tmpl.into(),
args: args, args: args,
} }
} }
}
impl<V: Into<String> + Clone> FormatRenderer for SimpleFormatter<V> {
/// Renders a formatter to a string or returns an error. /// Renders a formatter to a string or returns an error.
/// ///
/// If the formatter has the wrong number of arguments for the number of replacements /// 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. /// it will return an error. Otherwise it will return the formatted string.
pub fn render(&self, pos: &Position) -> Result<String, Box<dyn Error>> { fn render(&self, pos: &Position) -> Result<String, Box<dyn Error>> {
let mut buf = String::new(); let mut buf = String::new();
let mut should_escape = false; let mut should_escape = false;
let mut count = 0; let mut count = 0;
@ -74,28 +83,141 @@ impl<V: Into<String> + Clone> Formatter<V> {
} }
} }
pub struct ExpressionFormatter<'a> {
tmpl: String,
builder: RefCell<FileBuilder<'a>>,
}
impl<'a> ExpressionFormatter<'a> {
pub fn new<S: Into<String>>(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<Val, Box<dyn Error>> {
// 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<String, Box<dyn Error>> {
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)] #[cfg(test)]
mod test { mod test {
use super::Formatter; use super::{FormatRenderer, SimpleFormatter};
use crate::ast::Position; use crate::ast::Position;
#[test] #[test]
fn test_format_happy_path() { 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); let pos = Position::new(0, 0, 0);
assert_eq!(formatter.render(&pos).unwrap(), "foo bar quux @"); assert_eq!(formatter.render(&pos).unwrap(), "foo bar quux @");
} }
#[test] #[test]
fn test_format_happy_wrong_too_few_args() { 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); let pos = Position::new(0, 0, 0);
assert!(formatter.render(&pos).is_err()); assert!(formatter.render(&pos).is_err());
} }
#[test] #[test]
fn test_format_happy_wrong_too_many_args() { 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); let pos = Position::new(0, 0, 0);
assert!(formatter.render(&pos).is_err()); assert!(formatter.render(&pos).is_err());
} }

View File

@ -480,23 +480,35 @@ fn select_expression(input: SliceIter<Token>) -> Result<SliceIter<Token>, Expres
} }
} }
fn tuple_to_format(tok: Token, exprs: Vec<Expression>) -> Expression { make_fn!(
Expression::Format(FormatDef { simple_format_args<SliceIter<Token>, FormatArgs>,
template: tok.fragment.to_string(), do_each!(
args: exprs, _ => punct!("("),
pos: tok.pos, args => separated!(punct!(","), trace_parse!(expression)),
}) _ => must!(punct!(")")),
} (FormatArgs::List(args))
)
);
make_fn!(
expression_format_args<SliceIter<Token>, FormatArgs>,
do_each!(
expr => must!(expression),
(FormatArgs::Single(Box::new(expr)))
)
);
make_fn!( make_fn!(
format_expression<SliceIter<Token>, Expression>, format_expression<SliceIter<Token>, Expression>,
do_each!( do_each!(
tmpl => match_type!(STR), tmpl => match_type!(STR),
_ => punct!("%"), _ => punct!("%"),
_ => must!(punct!("(")), args => either!(simple_format_args, expression_format_args),
args => separated!(punct!(","), trace_parse!(expression)), (Expression::Format(FormatDef {
_ => must!(punct!(")")), template: tmpl.fragment.to_string(),
(tuple_to_format(tmpl, args)) args: args,
pos: tmpl.pos,
}))
) )
); );