From 5bfe38140a4e182186bbf9d2554741628eb2d847 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 20 Jan 2019 16:22:32 -0600 Subject: [PATCH] FEATURE: Schema checks for particular shapes. Progress toward #6 and #32. --- integration_tests/comparisons_test.ucg | 6 ++ src/build/mod.rs | 10 ++- std/schema.ucg | 85 ++++++++++++++++++++-- std/tests/schema_test.ucg | 99 +++++++++++++++++++++++++- 4 files changed, 187 insertions(+), 13 deletions(-) diff --git a/integration_tests/comparisons_test.ucg b/integration_tests/comparisons_test.ucg index 2e1605e..84ebbe8 100644 --- a/integration_tests/comparisons_test.ucg +++ b/integration_tests/comparisons_test.ucg @@ -213,4 +213,10 @@ assert { assert { ok = true || true == true, desc = "||: likes truth", +}; + +let name = "foo"; +assert { + ok = (name) in {foo="bar"}, + desc = "(name) in {foo=\"bar\"} works", }; \ No newline at end of file diff --git a/src/build/mod.rs b/src/build/mod.rs index 3eeec91..77cce25 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -769,11 +769,11 @@ impl<'a> FileBuilder<'a> { fn do_dot_lookup(&self, right: &Expression, scope: &Scope) -> Result, Box> { let pos = right.pos().clone(); + let scope = scope.clone().use_curr_val(); match right { - Expression::Copy(_) => return self.eval_expr(right, scope), - Expression::Call(_) => return self.eval_expr(right, scope), + Expression::Copy(_) => return self.eval_expr(right, &scope), + Expression::Call(_) => return self.eval_expr(right, &scope), Expression::Simple(Value::Symbol(ref s)) => { - let scope = scope.clone().use_curr_val(); scope .lookup_sym(s, true) .ok_or(Box::new(error::BuildError::new( @@ -783,7 +783,6 @@ impl<'a> FileBuilder<'a> { ))) } Expression::Simple(Value::Str(ref s)) => { - let scope = scope.clone().use_curr_val(); scope .lookup_sym(s, false) .ok_or(Box::new(error::BuildError::new( @@ -796,7 +795,7 @@ impl<'a> FileBuilder<'a> { scope.lookup_idx(right.pos(), &Val::Int(i.val)) } _ => { - let val = self.eval_expr(right, scope)?; + let val = self.eval_expr(right, &scope)?; match val.as_ref() { Val::Int(i) => scope.lookup_idx(right.pos(), &Val::Int(*i)), Val::Str(ref s) => scope @@ -968,7 +967,6 @@ 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()); - child_scope.search_curr_val = true; if let &BinaryExprType::DOT = kind { return self.do_dot_lookup(&def.right, &child_scope); }; diff --git a/std/schema.ucg b/std/schema.ucg index d6a1bad..477f5a5 100644 --- a/std/schema.ucg +++ b/std/schema.ucg @@ -1,15 +1,90 @@ -let one_of = module { +// The list of base types for a UCG value. +let base_types = [ + "int", + "float", + "str", + "bool", + "null", + "tuple", + "list", + "macro", + "module", +]; + +let base_type_reducer = macro(acc, f) => select (acc.val is f), f, { + true = acc{typ = f}, + false = acc, +}; + +// Computes the base type of a value. +let base_type_of = macro(val) => (reduce base_type_reducer {val=val, typ="null"}, base_types).typ; + +// Turns any schema check module into a compile failure. +// The module must export the computed value as the result field. +let must = macro(m, msg) => select m.result, fail msg, { + true = m.result, +}; + +// Any does a boolean match against a set of allowed shapes for a type. +// This module uses the shape module below so the same rules for checking +// the types against the source value apply for each example in the list. +let any = module { + // The source value to check. val=NULL, + // The set of allowed type shapes it can be. types=[], } => { + let schema = import "std/schema.ucg"; + let reducer = macro(acc, t) => acc{ - ok = acc.ok || (acc.val is t), + ok = acc.ok || (schema.shaped{val=acc.val, shape=t}.result), }; let any = macro(val, types) => reduce reducer {ok=false, val=val}, types; let result = any(mod.val, mod.types).ok; }; -let must = macro(m, msg) => select m.result, fail msg, { - true = m.result, -}; +// Compares a value against an example schema value. compares the "shape" of the +// value to see if it matches. The base type must be the same as the base type +// of the shape. For tuples any field in the shape tuple must be present in the +// source value and must be of the same base type and shape. This module will +// recurse into nested tuples. +// +// Lists are assumed to be able to contain any type and can be any length. +// We do not check that macros or modules have the same argument lengths or types +// nor we check that they output the same types. +let shaped = module { + // The source value to validate + val = NULL, + + // The shape to validate it against. + shape = NULL, + + // Whether partial matches are accepted. + // When set to true then the source value can have + // fields in tuples that are not present in the + // shape it is compared with. + partial = true, +} => { + let schema = import "std/schema.ucg"; + + let simple_handler = macro(val, shape) => val is (schema.base_type_of(shape)); + + let tuple_handler = macro(acc, name, value) => acc{ + ok = select (name) in acc.shape, mod.partial, { + true = schema.shaped{val=value, shape=acc.shape.(name)}.result, + }, + }; + + let result =select schema.base_type_of(mod.val), false, { + str = simple_handler(mod.val, mod.shape), + int = simple_handler(mod.val, mod.shape), + float = simple_handler(mod.val, mod.shape), + bool = simple_handler(mod.val, mod.shape), + null = simple_handler(mod.val, mod.shape), + tuple = (reduce tuple_handler {shape=mod.shape, ok=false}, (mod.val)).ok, + list = simple_handler(mod.val, mod.shape), + macro = simple_handler(mod.val, mod.shape), + module = simple_handler(mod.val, mod.shape), + }; +}; \ No newline at end of file diff --git a/std/tests/schema_test.ucg b/std/tests/schema_test.ucg index 4a9676d..35d67ff 100644 --- a/std/tests/schema_test.ucg +++ b/std/tests/schema_test.ucg @@ -2,11 +2,106 @@ let t = import "std/testing.ucg".asserts{}; let schema = import "std/schema.ucg"; assert t.ok{ - test = schema.must(schema.one_of{val=1, types=["float", "int"]}, "Must be a float or an int"), + test = schema.must(schema.any{val=1, types=[1.0, 1]}, "Must be a float or an int"), desc = "1 is an int", }; assert t.not_ok{ - test = schema.one_of{val=1, types=["float", "str"]}.result, + test = schema.any{val=1, types=[1.0, ""]}.result, desc = "1 is not a float or string", +}; + +assert t.equal{ + left = schema.base_type_of("foo"), + right = "str", +}; + +assert t.equal{ + left = schema.base_type_of(1), + right = "int", +}; + +assert t.equal{ + left = schema.base_type_of(1.0), + right = "float", +}; + +assert t.equal{ + left = schema.base_type_of(1.0), + right = "float", +}; + +assert t.equal{ + left = schema.base_type_of(true), + right = "bool", +}; + +assert t.equal{ + left = schema.base_type_of(NULL), + right = "null", +}; + +assert t.equal{ + left = schema.base_type_of({}), + right = "tuple", +}; + +assert t.equal{ + left = schema.base_type_of([]), + right = "list", +}; + +assert t.equal{ + left = schema.base_type_of(macro(arg) => arg), + right = "macro", +}; + +assert t.equal{ + left = schema.base_type_of(schema.any), + right = "module", +}; + +assert t.equal{ + left = schema.shaped{val="foo", shape=""}.result, + right = true, +}; + +assert t.ok{ + test = schema.shaped{val="foo", shape=""}.result, + desc = "\"foo\" is same shape as \"\"", +}; + +assert t.not_ok{ + test = schema.shaped{val="foo", shape=0}.result, + desc = "\"foo\" is not the same shape as 0", +}; + +assert t.ok{ + test = schema.shaped{val={foo="bar"}, shape={foo=""}}.result, + desc = "shaped for simple tuples works", +}; + +assert t.ok{ + test = schema.shaped{val={foo="bar", count=1}, shape={foo=""}}.result, + desc = "shaped for partial tuples works", +}; + +assert t.not_ok{ + test = schema.shaped{partial=false, val={foo="bar", count=1}, shape={foo=""}}.result, + desc = "shaped for non partial tuples also works", +}; + +assert t.ok{ + test = schema.shaped{val={foo="bar", inner={one=1}}, shape={foo="", inner={one=0}}}.result, + desc = "shaped for nested tuples works", +}; + +assert t.ok{ + test = schema.shaped{val={foo="bar", inner=[1, 2]}, shape={foo="", inner=[]}}.result, + desc = "shaped for nested list in tuple works", +}; + +assert t.not_ok{ + test = schema.shaped{val={inner={foo="bar"}}, shape={inner={foo=1}}}.result, + desc = "shaped fails when the shape doesn't match", }; \ No newline at end of file