mirror of
https://github.com/zaphar/ucg.git
synced 2025-07-23 18:29:50 -04:00
FEATURE: Expression format string support.
Experimental support for Issue #23
This commit is contained in:
parent
44055c28e9
commit
3c1b3ce86a
@ -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",
|
||||||
};
|
};
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
@ -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>> {
|
||||||
|
@ -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
|
||||||
|
138
src/format.rs
138
src/format.rs
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user