FEATURE: Schema checks for particular shapes.

Progress toward #6 and #32.
This commit is contained in:
Jeremy Wall 2019-01-20 16:22:32 -06:00
parent 6321828006
commit 5bfe38140a
4 changed files with 187 additions and 13 deletions

View File

@ -214,3 +214,9 @@ assert {
ok = true || true == true,
desc = "||: likes truth",
};
let name = "foo";
assert {
ok = (name) in {foo="bar"},
desc = "(name) in {foo=\"bar\"} works",
};

View File

@ -769,11 +769,11 @@ impl<'a> FileBuilder<'a> {
fn do_dot_lookup(&self, right: &Expression, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
let pos = right.pos().clone();
match right {
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();
match right {
Expression::Copy(_) => return self.eval_expr(right, &scope),
Expression::Call(_) => return self.eval_expr(right, &scope),
Expression::Simple(Value::Symbol(ref s)) => {
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);
};

View File

@ -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),
};
};

View File

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