diff --git a/docsite/site/content/reference/expressions.md b/docsite/site/content/reference/expressions.md index 2ea1d20..47dbd81 100644 --- a/docsite/site/content/reference/expressions.md +++ b/docsite/site/content/reference/expressions.md @@ -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 ---------------- diff --git a/integration_tests/types_test.ucg b/integration_tests/types_test.ucg new file mode 100644 index 0000000..58ebe21 --- /dev/null +++ b/integration_tests/types_test.ucg @@ -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", +}; \ No newline at end of file diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a7486f0..28d5bd4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -586,6 +586,13 @@ pub struct ImportDef { pub path: Token, } +#[derive(Debug, PartialEq, Clone)] +pub struct IsDef { + pub pos: Position, + pub target: Box, + 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, "")?; } + &Expression::IS(_) => { + write!(w, "")?; + } &Expression::Import(_) => { write!(w, "")?; } diff --git a/src/ast/walk.rs b/src/ast/walk.rs index 6a48e82..33236af 100644 --- a/src/ast/walk.rs +++ b/src/ast/walk.rs @@ -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()); diff --git a/src/build/compile_test.rs b/src/build/compile_test.rs index 70efcdf..8aaa4b3 100644 --- a/src/build/compile_test.rs +++ b/src/build/compile_test.rs @@ -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")); +} diff --git a/src/build/ir.rs b/src/build/ir.rs index c867cf1..8b9e452 100644 --- a/src/build/ir.rs +++ b/src/build/ir.rs @@ -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 { diff --git a/src/build/mod.rs b/src/build/mod.rs index 50456d5..218124e 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -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, Box> { + 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, Box> { @@ -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), } } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 1e03432..88ea9fe 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -650,6 +650,21 @@ make_fn!( ) ); +make_fn!( + is_type_expression, 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) -> ParseResult { let _input = input.clone(); either!( @@ -678,7 +693,7 @@ make_fn!( fn expression(input: SliceIter) -> ParseResult { 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), diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs index 728f666..77301aa 100644 --- a/src/tokenizer/mod.rs +++ b/src/tokenizer/mod.rs @@ -274,6 +274,10 @@ make_fn!(intok, do_text_token_tok!(TokenType::BAREWORD, "in", WS) ); +make_fn!(istok, + do_text_token_tok!(TokenType::BAREWORD, "is", WS) +); + make_fn!(macrotok, do_text_token_tok!(TokenType::BAREWORD, "macro", WS) ); @@ -408,6 +412,7 @@ fn token<'a>(input: OffsetStrIter<'a>) -> Result, Token> { rightsquarebracket, booleantok, intok, + istok, lettok, outtok, selecttok,