FEATURE: Add the is operator

This operator tests that something is of a certain base type.
This commit is contained in:
Jeremy Wall 2019-01-18 18:44:29 -06:00
parent 77075d6c79
commit 25d84a771e
9 changed files with 149 additions and 10 deletions

View File

@ -160,6 +160,15 @@ UCG binary operators follow the typical operator precedence for math. `*` and
`/` are higher precendence than `+` and `-` which are higher precedence than
any of the comparison operators.
Type test expressions
---------------------
ucg has the `is` operator for testing that something is of a given base type.
```
("foo" is str) == true;
```
Copy Expressions
----------------

View File

@ -0,0 +1,51 @@
let t = import "std/testing.ucg".asserts{};
assert t.ok{
test = "foo" is str,
desc = "foo is a str",
};
assert t.not_ok{
test = "foo" is int,
desc = "foo is not an int",
};
assert t.ok{
test = {foo="bar"} is tuple,
desc = "found a tuple",
};
assert t.not_ok{
test = {foo="bar"} is str,
desc = "a tuple is not a str",
};
assert t.ok{
test = [1, 2] is list,
desc = "found a list",
};
assert t.not_ok{
test = [1, 2] is tuple,
desc = "list is not a tuple",
};
assert t.ok{
test = (macro(arg) => arg) is macro,
desc = "found a macro",
};
assert t.not_ok{
test = (macro(arg) => arg) is list,
desc = "a macro is not a list",
};
assert t.ok{
test = (module{} => {}) is module,
desc = "found a module",
};
assert t.not_ok{
test = module{} => {} is macro,
desc = "a module is not a macro",
};

View File

@ -586,6 +586,13 @@ pub struct ImportDef {
pub path: Token,
}
#[derive(Debug, PartialEq, Clone)]
pub struct IsDef {
pub pos: Position,
pub target: Box<Expression>,
pub typ: Token,
}
/// Encodes a ucg expression. Expressions compute a value from.
#[derive(Debug, PartialEq, Clone)]
pub enum Expression {
@ -595,6 +602,9 @@ pub enum Expression {
// Binary expressions
Binary(BinaryOpDef),
// Type tests
IS(IsDef),
// Complex Expressions
Copy(CopyDef),
Range(RangeDef),
@ -627,6 +637,7 @@ impl Expression {
&Expression::FuncOp(ref def) => def.pos(),
&Expression::Include(ref def) => &def.pos,
&Expression::Import(ref def) => &def.pos,
&Expression::IS(ref def) => &def.pos,
}
}
}
@ -670,6 +681,9 @@ impl fmt::Display for Expression {
&Expression::Include(_) => {
write!(w, "<Include>")?;
}
&Expression::IS(_) => {
write!(w, "<IS>")?;
}
&Expression::Import(_) => {
write!(w, "<Include>")?;
}

View File

@ -103,6 +103,9 @@ impl<'a> AstWalker<'a> {
self.walk_expression(expr.as_mut());
}
}
Expression::IS(ref mut def) => {
self.walk_expression(def.target.as_mut());
}
Expression::Select(ref mut def) => {
self.walk_expression(def.default.as_mut());
self.walk_expression(def.val.as_mut());

View File

@ -96,3 +96,8 @@ fn test_concatenation() {
fn test_format() {
assert_build(include_str!("../../integration_tests/format_test.ucg"));
}
#[test]
fn test_type_checks() {
assert_build(include_str!("../../integration_tests/types_test.ucg"));
}

View File

@ -191,6 +191,19 @@ impl Val {
}
return false;
}
pub fn is_str(&self) -> bool {
if let &Val::Str(_) = self {
return true;
}
return false;
}
pub fn is_module(&self) -> bool {
if let &Val::Module(_) = self {
return true;
}
return false;
}
}
impl Display for Val {

View File

@ -518,7 +518,7 @@ impl<'a> FileBuilder<'a> {
}
Val::Str(ref s) => match right.as_ref() {
&Val::Str(ref ss) => {
return Ok(Rc::new(Val::Str([s.to_string(), ss.clone()].concat())))
return Ok(Rc::new(Val::Str([s.to_string(), ss.clone()].concat())));
}
val => {
return Err(Box::new(error::BuildError::new(
@ -531,7 +531,7 @@ impl<'a> FileBuilder<'a> {
),
error::ErrorType::TypeFail,
pos.clone(),
)))
)));
}
},
Val::List(ref l) => match right.as_ref() {
@ -552,7 +552,7 @@ impl<'a> FileBuilder<'a> {
),
error::ErrorType::TypeFail,
pos.clone(),
)))
)));
}
},
ref expr => {
@ -560,7 +560,7 @@ impl<'a> FileBuilder<'a> {
format!("{} does not support the '+' operation", expr.type_name()),
error::ErrorType::Unsupported,
pos.clone(),
)))
)));
}
}
}
@ -583,7 +583,7 @@ impl<'a> FileBuilder<'a> {
format!("{} does not support the '-' operation", expr.type_name()),
error::ErrorType::Unsupported,
pos.clone(),
)))
)));
}
}
}
@ -606,7 +606,7 @@ impl<'a> FileBuilder<'a> {
format!("{} does not support the '*' operation", expr.type_name()),
error::ErrorType::Unsupported,
pos.clone(),
)))
)));
}
}
}
@ -629,7 +629,7 @@ impl<'a> FileBuilder<'a> {
format!("{} does not support the '*' operation", expr.type_name()),
error::ErrorType::Unsupported,
pos.clone(),
)))
)));
}
}
}
@ -1530,7 +1530,7 @@ impl<'a> FileBuilder<'a> {
format!("Error finding file {} {}", path, e),
error::ErrorType::TypeFail,
pos.clone(),
)))
)));
}
};
let mut f = match File::open(&normalized) {
@ -1540,7 +1540,7 @@ impl<'a> FileBuilder<'a> {
format!("Error opening file {} {}", normalized.to_string_lossy(), e),
error::ErrorType::TypeFail,
pos.clone(),
)))
)));
}
};
let mut contents = String::new();
@ -1642,6 +1642,29 @@ impl<'a> FileBuilder<'a> {
Ok(Rc::new(Val::List(vec)))
}
pub fn eval_is_check(&self, def: &IsDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
let val = self.eval_expr(def.target.as_ref(), scope)?;
let result = match def.typ.fragment.as_str() {
"str" => val.is_str(),
"bool" => val.is_bool(),
"null" => val.is_empty(),
"int" => val.is_int(),
"float" => val.is_float(),
"tuple" => val.is_tuple(),
"list" => val.is_list(),
"macro" => val.is_macro(),
"module" => val.is_module(),
other => {
return Err(Box::new(error::BuildError::new(
format!("Expected valid type name but got {}", other),
error::ErrorType::TypeFail,
def.pos.clone(),
)));
}
};
Ok(Rc::new(Val::Boolean(result)))
}
// 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<Rc<Val>, Box<dyn Error>> {
@ -1665,6 +1688,7 @@ impl<'a> FileBuilder<'a> {
&Expression::FuncOp(ref def) => self.eval_func_op(def, scope),
&Expression::Include(ref def) => self.eval_include(def),
&Expression::Import(ref def) => self.eval_import(def),
&Expression::IS(ref def) => self.eval_is_check(def, scope),
}
}
}

View File

@ -650,6 +650,21 @@ make_fn!(
)
);
make_fn!(
is_type_expression<SliceIter<Token>, Expression>,
do_each!(
pos => pos,
expr => non_op_expression,
_ => word!("is"),
typ => must!(match_type!(BAREWORD)),
(Expression::IS(IsDef{
pos: pos,
target: Box::new(expr),
typ: typ,
}))
)
);
fn unprefixed_expression(input: SliceIter<Token>) -> ParseResult<Expression> {
let _input = input.clone();
either!(
@ -678,7 +693,7 @@ make_fn!(
fn expression(input: SliceIter<Token>) -> ParseResult<Expression> {
let _input = input.clone();
match trace_parse!(_input, op_expression) {
match trace_parse!(_input, either!(is_type_expression, op_expression)) {
Result::Incomplete(i) => Result::Incomplete(i),
Result::Fail(_) => trace_parse!(input, non_op_expression),
Result::Abort(e) => Result::Abort(e),

View File

@ -274,6 +274,10 @@ make_fn!(intok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::BAREWORD, "in", WS)
);
make_fn!(istok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::BAREWORD, "is", WS)
);
make_fn!(macrotok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::BAREWORD, "macro", WS)
);
@ -408,6 +412,7 @@ fn token<'a>(input: OffsetStrIter<'a>) -> Result<OffsetStrIter<'a>, Token> {
rightsquarebracket,
booleantok,
intok,
istok,
lettok,
outtok,
selecttok,