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 {
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",
};

View File

@ -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<Expression>),
Single(Box<Expression>),
}
/// Encodes a format expression in the UCG AST.
#[derive(Debug, PartialEq, Clone)]
pub struct FormatDef {
pub template: String,
pub args: Vec<Expression>,
pub args: FormatArgs,
pub pos: Position,
}

View File

@ -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());

View File

@ -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<Rc<Val>, Box<dyn Error>> {
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<Rc<Val>, Box<dyn Error>> {
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<Rc<Val>, Box<dyn Error>> {

View File

@ -97,8 +97,9 @@ impl Scope {
}
/// 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
}
/// Lookup up a list index in the current value

View File

@ -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<String, Box<dyn Error>>;
}
/// 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,
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.
pub fn new<S: Into<String>>(tmpl: S, args: Vec<V>) -> Self {
Formatter {
SimpleFormatter {
tmpl: tmpl.into(),
args: args,
}
}
}
impl<V: Into<String> + Clone> FormatRenderer for SimpleFormatter<V> {
/// 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<String, Box<dyn Error>> {
fn render(&self, pos: &Position) -> Result<String, Box<dyn Error>> {
let mut buf = String::new();
let mut should_escape = false;
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)]
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());
}

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 {
Expression::Format(FormatDef {
template: tok.fragment.to_string(),
args: exprs,
pos: tok.pos,
})
}
make_fn!(
simple_format_args<SliceIter<Token>, FormatArgs>,
do_each!(
_ => punct!("("),
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!(
format_expression<SliceIter<Token>, 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,
}))
)
);