From 72feff583cc76c1a74fa2467ebd7e9fb3c28ed6d Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Tue, 5 Apr 2016 14:26:37 -0700 Subject: [PATCH] Flesh out the grammar - Added expressions, packages, imports, rules, and modules to grammar - Added support for comments - Added a few failure cases to strings, arrays, and objects - Added IsGround interface to terms --- Makefile | 5 +- opalog/opalog.peg | 238 +++++++++++++++++++---- opalog/parser_ext.go | 135 +++++++++++++ opalog/parser_test.go | 431 +++++++++++++++++++++++++++++++++++++----- opalog/policy.go | 239 +++++++++++++++++++++++ opalog/policy_test.go | 272 ++++++++++++++++++++++++++ opalog/term.go | 145 ++++++++------ opalog/term_test.go | 12 ++ 8 files changed, 1334 insertions(+), 143 deletions(-) create mode 100644 opalog/parser_ext.go create mode 100644 opalog/policy.go create mode 100644 opalog/policy_test.go diff --git a/Makefile b/Makefile index cae6e99eff..1556679c5e 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ GO := go GO15VENDOREXPERIMENT := 1 export GO15VENDOREXPERIMENT -.PHONY: all deps generate build test check check-fmt check-vet check-lint clean +.PHONY: all deps generate build test check check-fmt check-vet check-lint fmt clean all: build test check @@ -47,5 +47,8 @@ check-vet: check-lint: ./build/check-lint.sh +fmt: + $(GO) fmt $(PACKAGES) + clean: rm -f ./opa diff --git a/opalog/opalog.peg b/opalog/opalog.peg index 21d1e5fdcc..4815cf08b7 100644 --- a/opalog/opalog.peg +++ b/opalog/opalog.peg @@ -5,19 +5,166 @@ package opalog // BUGS: the escaped forward solidus (`\/`) is not currently handled for strings. // +// currentLocation converts the parser context to a Location object. func currentLocation(c *current) *Location { - // TODO: Is it possible to propagate file names into the parser? + // TODO(tsandall): is it possible to access the filename from inside the parser? return NewLocation(c.text, "", c.pos.line, c.pos.col) } } -Prog <- _ head:Term tail:( ws Term )* EOF { - if head == nil { - return make([]interface{}, 0), nil +Program <- _ vals:(head:Stmt tail:(ws Stmt)*)? _ EOF { + var buf []interface{} + + if vals == nil { + return buf, nil } + + ifaceSlice := vals.([]interface{}) + head := ifaceSlice[0] + buf = append(buf, head) + for _, tail := range ifaceSlice[1].([]interface{}) { + stmt := tail.([]interface{})[1] + buf = append(buf, stmt) + } + + return buf, nil +} + +Stmt <- val:(Package / Import / Rule / Body / Comment) { + return val, nil +} + +Package <- "package" ws val:(Ref / Var) { + // All packages are implicitly declared under the default root document. + path := RefTerm(DefaultRootDocument) + switch v := val.(*Term).Value.(type) { + case Ref: + // Convert head of package Ref to String because it will be prefixed + // with the root document variable. + head := v[0] + head = StringTerm(string(head.Value.(Var))) + head.Location = v[0].Location + tail := v[1:] + if !tail.IsGround() { + return nil, fmt.Errorf("package name cannot contain variables: %v", v) + } + + // We do not allow non-string values in package names. + // Because documents are typically represented as JSON, non-string keys are + // not allowed for now. + // TODO(tsandall): consider special syntax for namespacing under arrays. + for _, p := range tail { + _, ok := p.Value.(String) + if !ok { + return nil, fmt.Errorf("package name cannot contain non-string values: %v", v) + } + } + path.Value = append(path.Value.(Ref), head) + path.Value = append(path.Value.(Ref), tail...) + case Var: + s := StringTerm(string(v)) + s.Location = val.(*Term).Location + path.Value = append(path.Value.(Ref), s) + } + pkg := &Package{Location: currentLocation(c), Path: path.Value.(Ref)} + return pkg, nil +} + +Import <- "import" ws path:(Ref / Var) alias:(ws "as" ws Var)? { + imp := &Import{} + imp.Location = currentLocation(c) + imp.Path = path.(*Term).Value + switch p := imp.Path.(type) { + case Ref: + if !p[1:].IsGround() { + return nil, fmt.Errorf("import cannot contain variables in tail: %v", p) + } + } + if alias == nil { + return imp, nil + } + aliasSlice := alias.([]interface{}) + // Import definition above describes the "alias" slice. We only care about the "Var" element. + imp.Alias = aliasSlice[3].(*Term).Value.(Var) + return imp, nil +} + +// TODO(tsandall): update to handle underscore variables +Rule <- name:Var key:( _ "[" _ Term _ "]" _ )? value:( _ "=" _ Term )? body:( _ ":-" _ Body) { + + if key == nil && value == nil { + return nil, fmt.Errorf("rule head must include key and/or value: %v", name) + } + + rule := &Rule{} + rule.Location = currentLocation(c) + rule.Name = name.(*Term).Value.(Var) + + if key != nil { + keySlice := key.([]interface{}) + // Rule definition above describes the "key" slice. We care about the "Term" element. + rule.Key = keySlice[3].(*Term) + _, ok := rule.Key.Value.(Var) + if !ok { + return nil, fmt.Errorf("rule key must be a variable: %v", rule.Key) + } + } + + if value != nil { + valueSlice := value.([]interface{}) + // Rule definition above describes the "value" slice. We care about the "Term" element. + rule.Value = valueSlice[len(valueSlice)-1].(*Term) + } + + // Rule definition above describes the "body" slice. We only care about the "Body" element. + rule.Body = body.([]interface{})[3].(Body) + + return rule, nil +} + +Body <- head:Expr tail:( _ "," _ Expr)* { + var buf Body + buf = append(buf, head.(*Expr)) + for _, s := range tail.([]interface{}) { + expr := s.([]interface{})[3].(*Expr) + buf = append(buf, expr) + } + return buf, nil +} + +Expr <- neg:( "not" ws )? val:(InfixExpr / PrefixExpr / Term) { + expr := &Expr{} + expr.Location = currentLocation(c) + expr.Negated = neg != nil + expr.Terms = val + return expr, nil +} + +InfixExpr <- left:Term _ op:InfixOp _ right:Term { + return []*Term{op.(*Term), left.(*Term), right.(*Term)}, nil +} + +InfixOp <- val:("=" / "!=" / "<=" / ">=" / "<" / ">") { + operator := VarTerm(string(c.text)) + operator.Location = currentLocation(c) + return operator, nil +} + +PrefixExpr <- op:Var "(" _ head:Term? tail:( _ "," _ Term )* _ ")" { + buf := []*Term{op.(*Term)} + if head == nil { + return buf, nil + } + buf = append(buf, head.(*Term)) + + // PrefixExpr above describes the "tail" structure. We only care about the "Term" elements. tailSlice := tail.([]interface{}) - return append([]interface{}{head}, tailSlice...), nil + for _, v := range tailSlice { + s := v.([]interface{}) + buf = append(buf, s[len(s) - 1].(*Term)) + } + return buf, nil } Term <- val:( Composite / Scalar / Ref / Var ) { @@ -31,103 +178,120 @@ Scalar <- Number / String / Bool / Null Key <- Scalar / Ref / Var Object <- '{' _ head:(Key _ ':' _ Term)? tail:( _ ',' _ Key _ ':' _ Term )* _ '}' { - var buf [][2]*Term + obj := ObjectTerm() + obj.Location = currentLocation(c) // Empty object. if head == nil { - return ObjectTermWithLoc(buf, currentLocation(c)), nil + return obj, nil } - // Non-empty object, first key/value pair. - // The "head" variable is a slice containing exactly 5 elements (see rule definition above): - // [key whitespace colon whitespace key] where the whitespace elements may be nil. + // Object definition above describes the "head" structure. We only care about the "Key" and "Term" elements. headSlice := head.([]interface{}) - buf = append(buf, Item(headSlice[0].(*Term), headSlice[len(headSlice) - 1].(*Term))) + obj.Value = append(obj.Value.(Object), Item(headSlice[0].(*Term), headSlice[len(headSlice) - 1].(*Term))) // Non-empty object, remaining key/value pairs. tailSlice := tail.([]interface{}) for _, v := range tailSlice { s := v.([]interface{}) - // The "s" variable is a slice containing exactly 8 elements (see rule definition above). - // This is similar to the "head" variable." - buf = append(buf, Item(s[3].(*Term), s[len(s) - 1].(*Term))) + // Object definition above describes the "tail" structure. We only care about the "Key" and "Term" elements. + obj.Value = append(obj.Value.(Object), Item(s[3].(*Term), s[len(s) - 1].(*Term))) } - return ObjectTermWithLoc(buf, currentLocation(c)), nil + return obj, nil } Array <- '[' _ head:Term? tail:(_ ',' _ Term)* _ ']' { - var buf []*Term + arr := ArrayTerm() + arr.Location = currentLocation(c) // Empty array. if head == nil { - return ArrayTermWithLoc(buf, currentLocation(c)), nil + return arr, nil } // Non-empty array, first element. - buf = append(buf, head.(*Term)) + arr.Value = append(arr.Value.(Array), head.(*Term)) // Non-empty array, remaining elements. tailSlice := tail.([]interface{}) for _, v := range tailSlice { s := v.([]interface{}) - // The "s" is a slice containing exactly 4 elements (see rule definition above). - // [whitespace comma whitespace value] where the whitespace elements may be nil. - buf = append(buf, s[len(s) - 1].(*Term)) + // Array definition above describes the "tail" structure. We only care about the "Term" elements. + arr.Value = append(arr.Value.(Array), s[len(s) - 1].(*Term)) } - return ArrayTermWithLoc(buf, currentLocation(c)), nil + return arr, nil } Ref <- head:Var tail:( RefDot / RefBracket )+ { - buf := []*Term{head.(*Term)} + + ref := RefTerm(head.(*Term)) + ref.Location = currentLocation(c) tailSlice := tail.([]interface{}) for _, v := range tailSlice { - buf = append(buf, v.(*Term)) + ref.Value = append(ref.Value.(Ref), v.(*Term)) } - return RefTermWithLoc(buf, currentLocation(c)), nil + return ref, nil } RefDot <- "." val:Var { // Convert the Var into a string because 'foo.bar.baz' is equivalent to 'foo["bar"]["baz"]'. - return StringTermWithLoc(string(val.(*Term).Value.(Var)), currentLocation(c)), nil + str := StringTerm(string(val.(*Term).Value.(Var))) + str.Location = currentLocation(c) + return str, nil } RefBracket <- "[" val:(Scalar / Var) "]" { return val, nil } -Var <- vals:( AsciiLetter (AsciiLetter / DecimalDigit)* ) { - return VarTermWithLoc(string(c.text), currentLocation(c)), nil +Var <- !Reserved AsciiLetter (AsciiLetter / DecimalDigit)* { + str := string(c.text) + variable := VarTerm(str) + variable.Location = currentLocation(c) + return variable, nil } Number <- '-'? Integer ( '.' DecimalDigit+ )? Exponent? { // JSON numbers have the same syntax as Go's, and are parseable using // strconv. v, err := strconv.ParseFloat(string(c.text), 64) - return NumberTermWithLoc(v, currentLocation(c)), err + num := NumberTerm(v) + num.Location = currentLocation(c) + return num, err } String <- '"' ( !EscapedChar . / '\\' EscapeSequence )* '"' { // TODO : the forward slash (solidus) is not a valid escape in Go, it will // fail if there's one in the string v, err := strconv.Unquote(string(c.text)) - return StringTermWithLoc(v, currentLocation(c)), err + str := StringTerm(v) + str.Location = currentLocation(c) + return str, err } Bool <- "true" { - return BooleanTermWithLoc(true, currentLocation(c)), nil + bol := BooleanTerm(true) + bol.Location = currentLocation(c) + return bol, nil } / "false" { - return BooleanTermWithLoc(false, currentLocation(c)), nil + bol := BooleanTerm(false) + bol.Location = currentLocation(c) + return bol, nil } Null <- "null" { - return NullTermWithLoc(currentLocation(c)), nil + null := NullTerm() + null.Location = currentLocation(c) + return null, nil } +Reserved <- ("not" / "package" / "import" / "null" / "true" / "false") + Integer <- '0' / NonZeroDecimalDigit DecimalDigit* Exponent <- 'e'i [+-]? DecimalDigit+ @@ -146,10 +310,12 @@ DecimalDigit <- [0-9] NonZeroDecimalDigit <- [1-9] -HexDigit <- [0-9a-f]i - -_ "whitespace" <- [ \t\r\n]* +HexDigit <- [0-9a-f] ws "whitespace" <- [ \t\r\n]+ +_ "whitespace" <- ( [ \t\r\n] / Comment )* + +Comment <- [ \t]* "#" [^\r\n]* + EOF <- !. diff --git a/opalog/parser_ext.go b/opalog/parser_ext.go new file mode 100644 index 0000000000..8153d99d60 --- /dev/null +++ b/opalog/parser_ext.go @@ -0,0 +1,135 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +// This file contains extra functions for parsing Opalog. +// Most of the parsing is handled by the auto-generated code in +// parser.go, however, there are additional utilities that are +// helpful for dealing with Opalog source inputs (e.g., REPL +// statements, source files, etc.) + +package opalog + +import "fmt" + +// MustParseStatements returns a slice of parsed statements. +// If an error occurs during parsing, an exception is raised. +func MustParseStatements(input string) []interface{} { + parsed, err := ParseStatements(input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseStatement returns exactly one statement. +// If an error occurs during parsing or multiple statements are parsed, +// panic(err) is called. +func MustParseStatement(input string) interface{} { + parsed, err := ParseStatement(input) + if err != nil { + panic(err) + } + return parsed +} + +// ParseStatements returns a slice of parsed statements. +// This is the default return value from the parser. +func ParseStatements(input string) ([]interface{}, error) { + parsed, err := Parse("", []byte(input)) + if err != nil { + return nil, err + } + stmts := parsed.([]interface{}) + return stmts, err +} + +// ParseStatement returns exactly one statement. +// A statement might be a term, expression, rule, etc. Regardless, +// this function expects *exactly* one statement. If multiple +// statements are parsed, an error is returned. +func ParseStatement(input string) (interface{}, error) { + stmts, err := ParseStatements(input) + if err != nil { + return nil, err + } + if len(stmts) != 1 { + return nil, fmt.Errorf("expected exactly one statement") + } + return stmts[0], nil +} + +// ParseModule returns a parsed Module object. +// For details on Module objects and their fields, see policy.go. +// Empty input will return nil, nil. +func ParseModule(input string) (*Module, error) { + stmts, err := ParseStatements(input) + if err != nil { + return nil, err + } + if len(stmts) == 0 { + return nil, nil + } + + _package, ok := stmts[0].(*Package) + if !ok { + return nil, fmt.Errorf("first statement must be package") + } + + mod := &Module{ + Package: _package, + } + + for _, stmt := range stmts[1:] { + switch stmt := stmt.(type) { + case *Import: + mod.Imports = append(mod.Imports, stmt) + case *Rule: + mod.Rules = append(mod.Rules, stmt) + case Body: + rule, err := parseConstantRule(stmt) + if err != nil { + return nil, err + } + mod.Rules = append(mod.Rules, rule) + } + } + + return mod, nil +} + +// parseConstantRule attempts to return a rule from a Body. +// Equality expressions of the form = can be +// converted into rules of the form = :- true. +// This is a concise way of defining constants inside modules. +// This function handles the conversion. +func parseConstantRule(stmt Body) (*Rule, error) { + if len(stmt) > 1 { + return nil, fmt.Errorf("expression must be contained inside rule: %v", stmt) + } else if len(stmt) == 1 { + stmt := stmt[0] + if !stmt.IsEquality() { + return nil, fmt.Errorf("non-equality expression must be contained inside rule: %v", stmt) + } + terms := stmt.Terms.([]*Term) + if !terms[2].IsGround() { + return nil, fmt.Errorf("constant rule value must be ground: %v", stmt) + } + switch name := terms[1].Value.(type) { + case Var: + rule := &Rule{ + Location: stmt.Location, + Name: name, + Value: terms[2], + Body: []*Expr{ + &Expr{Terms: BooleanTerm(true)}, + }, + } + return rule, nil + default: + return nil, fmt.Errorf("rule name must be a variable: %v", stmt) + } + } else { + panic("unreachable") + } +} diff --git a/opalog/parser_test.go b/opalog/parser_test.go index 25ca9efc3d..eca7777d92 100644 --- a/opalog/parser_test.go +++ b/opalog/parser_test.go @@ -22,30 +22,41 @@ func TestScalarTerms(t *testing.T) { assertParseOneTerm(t, "exponent", "6e7", NumberTerm(6e7)) assertParseOneTerm(t, "string", "\"a string\"", StringTerm("a string")) assertParseOneTerm(t, "string", "\"a string u6abc7def8abc0def with unicode\"", StringTerm("a string u6abc7def8abc0def with unicode")) - assertParseOneTermFail(t, "hex", "6abc") - assertParseOneTermFail(t, "non-string", "'a string'") - assertParseOneTermFail(t, "non-number", "6zxy") - assertParseOneTermFail(t, "non-number2", "6d7") - assertParseOneTermFail(t, "non-number3", "6\"foo\"") - assertParseOneTermFail(t, "non-number4", "6true") - assertParseOneTermFail(t, "non-number5", "6false") - assertParseOneTermFail(t, "non-number6", "6[null, null]") - assertParseOneTermFail(t, "non-number7", "6{\"foo\": \"bar\"}") - assertParseOneTermFail(t, "out-of-range", "1e1000") + assertParseError(t, "hex", "6abc") + assertParseError(t, "non-terminated", "\"foo") + assertParseError(t, "non-string", "'a string'") + assertParseError(t, "non-number", "6zxy") + assertParseError(t, "non-number2", "6d7") + assertParseError(t, "non-number3", "6\"foo\"") + assertParseError(t, "non-number4", "6true") + assertParseError(t, "non-number5", "6false") + assertParseError(t, "non-number6", "6[null, null]") + assertParseError(t, "non-number7", "6{\"foo\": \"bar\"}") + assertParseError(t, "out-of-range", "1e1000") } func TestVarTerms(t *testing.T) { assertParseOneTerm(t, "var", "foo", VarTerm("foo")) assertParseOneTerm(t, "var", "foo_bar", VarTerm("foo_bar")) assertParseOneTerm(t, "var", "foo0", VarTerm("foo0")) - assertParseOneTermFail(t, "non-var", "foo-bar") - assertParseOneTermFail(t, "non-var2", "foo-7") + assertParseError(t, "non-var", "foo-bar") + assertParseError(t, "non-var2", "foo-7") + for _, v := range Keywords { + assertParseError(t, "keyword", v) + } } func TestRefTerms(t *testing.T) { assertParseOneTerm(t, "constants", "foo.bar.baz", RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz"))) assertParseOneTerm(t, "constants 2", "foo.bar[0].baz", RefTerm(VarTerm("foo"), StringTerm("bar"), NumberTerm(0), StringTerm("baz"))) assertParseOneTerm(t, "variables", "foo.bar[0].baz[i]", RefTerm(VarTerm("foo"), StringTerm("bar"), NumberTerm(0), StringTerm("baz"), VarTerm("i"))) + assertParseOneTerm(t, "spaces", "foo[\"white space\"].bar", RefTerm(VarTerm("foo"), StringTerm("white space"), StringTerm("bar"))) + assertParseError(t, "missing component 1", "foo.") + assertParseError(t, "missing component 2", "foo[].bar") + assertParseError(t, "composite operand 1", "foo[[1,2,3]].bar") + assertParseError(t, "composite operand 2", "foo[{1: 2}].bar") + // TODO(tsandall): this may be allowed some day + assertParseError(t, "nested refs", "foo[baz.qux].bar") } func TestObjectWithScalars(t *testing.T) { @@ -63,6 +74,15 @@ func TestObjectWithVars(t *testing.T) { assertParseOneTerm(t, "nested var keys", "{baz: {foo: \"bar\", bar: qux}}", ObjectTerm(Item(VarTerm("baz"), ObjectTerm(Item(VarTerm("foo"), StringTerm("bar")), Item(VarTerm("bar"), VarTerm("qux")))))) } +func TestObjectFail(t *testing.T) { + assertParseError(t, "non-terminated 1", "{foo: bar, baz: [], qux: corge") + assertParseError(t, "non-terminated 2", "{foo: bar, baz: [], qux: ") + assertParseError(t, "non-terminated 3", "{foo: bar, baz: [], qux ") + assertParseError(t, "non-terminated 4", "{foo: bar, baz: [], ") + assertParseError(t, "missing separator", "{foo: bar baz: []}") + assertParseError(t, "missing start", "foo: bar, baz: [], qux: corge}") +} + func TestArrayWithScalars(t *testing.T) { assertParseOneTerm(t, "number", "[1,2,3,4.5]", ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3), NumberTerm(4.5))) assertParseOneTerm(t, "bool", "[true, false, true]", ArrayTerm(BooleanTerm(true), BooleanTerm(false), BooleanTerm(true))) @@ -75,6 +95,14 @@ func TestArrayWithVars(t *testing.T) { assertParseOneTerm(t, "nested var elements", "[[foo, true], [null, bar], 42]", ArrayTerm(ArrayTerm(VarTerm("foo"), BooleanTerm(true)), ArrayTerm(NullTerm(), VarTerm("bar")), NumberTerm(42))) } +func TestArrayFail(t *testing.T) { + assertParseError(t, "non-terminated 1", "[foo, bar") + assertParseError(t, "non-terminated 2", "[foo, bar, ") + assertParseError(t, "missing element", "[foo, bar, ]") + assertParseError(t, "missing separator", "[foo bar]") + assertParseError(t, "missing start", "foo, bar, baz]") +} + func TestEmptyComposites(t *testing.T) { assertParseOneTerm(t, "empty object", "{}", ObjectTerm()) assertParseOneTerm(t, "emtpy array", "[]", ArrayTerm()) @@ -91,48 +119,355 @@ func TestCompositesWithRefs(t *testing.T) { assertParseOneTerm(t, "ref values", "[{8: a[i].b, f: c[0][\"d\"].e[j]}]", ArrayTerm(ObjectTerm(Item(NumberTerm(8), ref1), Item(VarTerm("f"), ref2)))) } -func assertTermEqual(t *testing.T, x *Term, y *Term) { - if !x.Equal(y) { - t.Errorf("Failure on equality: \n%s and \n%s\n", x, y) - } +func TestInfixExpr(t *testing.T) { + assertParseOneExpr(t, "scalars 1", "true = false", NewBuiltinExpr(VarTerm("="), BooleanTerm(true), BooleanTerm(false))) + assertParseOneExpr(t, "scalars 2", "3.14 = null", NewBuiltinExpr(VarTerm("="), NumberTerm(3.14), NullTerm())) + assertParseOneExpr(t, "scalars 3", "42 = \"hello world\"", NewBuiltinExpr(VarTerm("="), NumberTerm(42), StringTerm("hello world"))) + assertParseOneExpr(t, "vars 1", "hello = world", NewBuiltinExpr(VarTerm("="), VarTerm("hello"), VarTerm("world"))) + assertParseOneExpr(t, "vars 2", "42 = hello", NewBuiltinExpr(VarTerm("="), NumberTerm(42), VarTerm("hello"))) + + ref1 := RefTerm(VarTerm("foo"), NumberTerm(0), StringTerm("bar"), VarTerm("x")) + ref2 := RefTerm(VarTerm("baz"), BooleanTerm(false), StringTerm("qux"), StringTerm("hello")) + assertParseOneExpr(t, "refs 1", "foo[0].bar[x] = baz[false].qux[\"hello\"]", NewBuiltinExpr(VarTerm("="), ref1, ref2)) + + left1 := ObjectTerm(Item(VarTerm("a"), ArrayTerm(ref1))) + right1 := ArrayTerm(ObjectTerm(Item(NumberTerm(42), BooleanTerm(true)))) + assertParseOneExpr(t, "composites", "{a: [foo[0].bar[x]]} = [{42: true}]", NewBuiltinExpr(VarTerm("="), left1, right1)) + + assertParseOneExpr(t, "ne", "100 != 200", NewBuiltinExpr(VarTerm("!="), NumberTerm(100), NumberTerm(200))) + assertParseOneExpr(t, "gt", "17.4 > \"hello\"", NewBuiltinExpr(VarTerm(">"), NumberTerm(17.4), StringTerm("hello"))) + assertParseOneExpr(t, "lt", "17.4 < \"hello\"", NewBuiltinExpr(VarTerm("<"), NumberTerm(17.4), StringTerm("hello"))) + assertParseOneExpr(t, "gte", "17.4 >= \"hello\"", NewBuiltinExpr(VarTerm(">="), NumberTerm(17.4), StringTerm("hello"))) + assertParseOneExpr(t, "lte", "17.4 <= \"hello\"", NewBuiltinExpr(VarTerm("<="), NumberTerm(17.4), StringTerm("hello"))) + + left2 := ArrayTerm(ObjectTerm(Item(NumberTerm(14.2), BooleanTerm(true)), Item(StringTerm("a"), NullTerm()))) + right2 := ObjectTerm(Item(VarTerm("foo"), ObjectTerm(Item(RefTerm(VarTerm("a"), StringTerm("b"), NumberTerm(0)), ArrayTerm(NumberTerm(10)))))) + assertParseOneExpr(t, "composites", "[{14.2: true, \"a\": null}] != {foo: {a.b[0]: [10]}}", NewBuiltinExpr(VarTerm("!="), left2, right2)) } -func assertTermNotEqual(t *testing.T, x *Term, y *Term) { - if x.Equal(y) { - t.Errorf("Failure on non-equality: \n%s and \n%s\n", x, y) - } +func TestMiscBuiltinExpr(t *testing.T) { + xyz := VarTerm("xyz") + assertParseOneExpr(t, "empty", "xyz()", NewBuiltinExpr(xyz)) + assertParseOneExpr(t, "single", "xyz(abc)", NewBuiltinExpr(xyz, VarTerm("abc"))) + assertParseOneExpr(t, "multiple", "xyz(abc, {\"one\": [1,2,3]})", NewBuiltinExpr(xyz, VarTerm("abc"), ObjectTerm(Item(StringTerm("one"), ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3)))))) } -func assertParseOneTerm(t *testing.T, msg string, expr string, correct *Term) interface{} { - p, err := Parse("", []byte(expr)) - if err != nil { - t.Errorf("Error on test %s: parse error on %s: %s", msg, expr, err) - return nil - } - parsed := p.([]interface{}) - if len(parsed) != 1 { - t.Errorf("Error on test %s: failed to parse 1 element from %s: %v", - msg, expr, parsed) - return nil - } - term := parsed[0].(*Term) - if !term.Equal(correct) { - t.Errorf("Error on test %s: wrong result on %s. Actual = %v; Correct = %v", - msg, expr, term, correct) - return nil - } - return parsed[0] -} - -func assertParseOneTermFail(t *testing.T, msg string, expr string) { - p, err := Parse("", []byte(expr)) +func TestNegatedExpr(t *testing.T) { + assertParseOneTermNegated(t, "scalars 1", "not true", BooleanTerm(true)) + assertParseOneTermNegated(t, "scalars 2", "not \"hello\"", StringTerm("hello")) + assertParseOneTermNegated(t, "scalars 3", "not 100", NumberTerm(100)) + assertParseOneTermNegated(t, "scalars 4", "not null", NullTerm()) + assertParseOneTermNegated(t, "var", "not x", VarTerm("x")) + assertParseOneTermNegated(t, "ref", "not x[y].z", RefTerm(VarTerm("x"), VarTerm("y"), StringTerm("z"))) + assertParseOneExprNegated(t, "vars", "not x = y", NewBuiltinExpr(VarTerm("="), VarTerm("x"), VarTerm("y"))) + + ref1 := RefTerm(VarTerm("x"), VarTerm("y"), StringTerm("z"), VarTerm("a")) + + assertParseOneExprNegated(t, "membership", "not x[y].z[a] = \"b\"", NewBuiltinExpr(VarTerm("="), ref1, StringTerm("b"))) + assertParseOneExprNegated(t, "misc. builtin", "not sorted(x[y].z[a])", NewBuiltinExpr(VarTerm("sorted"), ref1)) +} + +func TestPackage(t *testing.T) { + ref1 := RefTerm(DefaultRootDocument, StringTerm("foo")) + assertParsePackage(t, "single", "package foo", &Package{Path: ref1.Value.(Ref)}) + ref2 := RefTerm(DefaultRootDocument, StringTerm("f00"), StringTerm("bar_baz"), StringTerm("qux")) + assertParsePackage(t, "multiple", "package f00.bar_baz.qux", &Package{Path: ref2.Value.(Ref)}) + ref3 := RefTerm(DefaultRootDocument, StringTerm("foo"), StringTerm("bar baz")) + assertParsePackage(t, "space", "package foo[\"bar baz\"]", &Package{Path: ref3.Value.(Ref)}) + assertParseError(t, "non-ground ref", "package foo[x]") + assertParseError(t, "non-string value", "package foo.bar[42].baz") +} + +func TestImport(t *testing.T) { + assertParseImport(t, "single", "import foo", &Import{Path: VarTerm("foo").Value}) + ref := RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value + assertParseImport(t, "multiple", "import foo.bar.baz", &Import{Path: ref}) + assertParseImport(t, "single alias", "import foo as bar", &Import{Path: VarTerm("foo").Value, Alias: Var("bar")}) + assertParseImport(t, "multiple alias", "import foo.bar.baz as qux", &Import{Path: ref, Alias: Var("qux")}) + ref2 := RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("white space")).Value + assertParseImport(t, "white space", "import foo.bar[\"white space\"]", &Import{Path: ref2}) + assertParseError(t, "non-ground ref", "import foo[x]") +} + +func TestRule(t *testing.T) { + + assertParseRule(t, "identity", "p = true :- true", &Rule{ + Name: Var("p"), + Value: BooleanTerm(true), + Body: []*Expr{ + &Expr{Terms: BooleanTerm(true)}, + }, + }) + + assertParseRule(t, "set", "p[x] :- x = 42", &Rule{ + Name: Var("p"), + Key: VarTerm("x"), + Body: []*Expr{ + NewBuiltinExpr(VarTerm("="), VarTerm("x"), NumberTerm(42)), + }, + }) + + assertParseRule(t, "object", "p[x] = y :- x = 42, y = \"hello\"", &Rule{ + Name: Var("p"), + Key: VarTerm("x"), + Value: VarTerm("y"), + Body: []*Expr{ + NewBuiltinExpr(VarTerm("="), VarTerm("x"), NumberTerm(42)), + NewBuiltinExpr(VarTerm("="), VarTerm("y"), StringTerm("hello")), + }, + }) + + assertParseRule(t, "constant composite", "p = [{\"foo\": [1,2,3,4]}] :- true", &Rule{ + Name: Var("p"), + Value: ArrayTerm( + ObjectTerm(Item(StringTerm("foo"), ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3), NumberTerm(4)))), + ), + Body: []*Expr{ + &Expr{Terms: BooleanTerm(true)}, + }, + }) + + assertParseError(t, "missing key and/or value", "p :- true") + assertParseError(t, "constant key", "p[100] :- true") + assertParseError(t, "composite key", "p[[1,2,x]] :- x = true") + assertParseError(t, "dangling comma", "p :- true, false,") +} + +func TestEmptyModule(t *testing.T) { + r, err := ParseModule(" ") if err != nil { + t.Errorf("Expected nil for empty module: %s", err) return } - parsed := p.([]interface{}) - if len(parsed) != 1 { - t.Errorf("Error on test %s: failed to parse 1 element from %s: %v", msg, expr, parsed) - } else { - t.Errorf("Error on test %s: failed to error when parsing %v: %v", msg, expr, parsed) + if r != nil { + t.Errorf("Expected nil for empty module: %v", r) } } + +func TestComments(t *testing.T) { + + testModule := ` + package a.b.c + + import e.f as g # end of line + import h + + # by itself + + p[x] = y :- y = "foo", + # inside a rule + x = "bar", + x != y, + q[x] + + import xyz.abc + + q # interruptting + [a] # the head of a rule + :- m = [1,2, + 3], + a = m[i] + ` + + assertParseModule(t, "module comments", testModule, &Module{ + Package: MustParseStatement("package a.b.c").(*Package), + Imports: []*Import{ + MustParseStatement("import e.f as g").(*Import), + MustParseStatement("import h").(*Import), + MustParseStatement("import xyz.abc").(*Import), + }, + Rules: []*Rule{ + MustParseStatement("p[x] = y :- y = \"foo\", x = \"bar\", x != y, q[x]").(*Rule), + MustParseStatement("q[a] :- m = [1,2,3], a = m[i]").(*Rule), + }, + }) +} + +func TestExample(t *testing.T) { + testModule := ` +package opa.examples # this policy belongs the opa.examples package + +import data.servers # import the data.servers document to refer to it as "servers" instead of "data.servers" +import data.networks # same but for data.networks +import data.ports # same but for data.ports + +violations[server] :- # a server exists in the violations set if: + server = servers[_], # the server exists in the servers collection + server.protocols[_] = "http", # and the server has http in its protocols collection + public_servers[server] # and the server exists in the public_servers set + +public_servers[server] :- # a server exists in the public_servers set if: + server = servers[_], # the server exists in the servers collection + server.ports[_] = ports[i].id, # and the server is connected to a port in the ports collection + ports[i].networks[_] = networks[j].id, # and the port is connected to a network in the networks collection + networks[j].public = true # and the network is public + ` + + assertParseModule(t, "example module", testModule, &Module{ + Package: MustParseStatement("package opa.examples").(*Package), + Imports: []*Import{ + MustParseStatement("import data.servers").(*Import), + MustParseStatement("import data.networks").(*Import), + MustParseStatement("import data.ports").(*Import), + }, + Rules: []*Rule{ + MustParseStatement(`violations[server] :- + server = servers[_], + server.protocols[_] = "http", + public_servers[server]`).(*Rule), + MustParseStatement(`public_servers[server] :- + server = servers[_], + server.ports[_] = ports[i].id, + ports[i].networks[_] = networks[j].id, + networks[j].public = true`).(*Rule), + }, + }) +} + +func TestConstantRules(t *testing.T) { + testModule := ` + package a.b.c + + pi = 3.14159 + + # intersperse a regular rule + p[x] :- x = 1 + + greeting = "hello" + + cores = [{0: 1}, {1: 2}] + ` + + assertParseModule(t, "constant rules", testModule, &Module{ + Package: MustParseStatement("package a.b.c").(*Package), + Rules: []*Rule{ + MustParseStatement("pi = 3.14159 :- true").(*Rule), + MustParseStatement("p[x] :- x = 1").(*Rule), + MustParseStatement("greeting = \"hello\" :- true").(*Rule), + MustParseStatement("cores = [{0: 1}, {1: 2}] :- true").(*Rule), + }, + }) + + multipleExprs := ` + package a.b.c + + pi = 3.14159, pi > 3 + ` + + nonEquality := ` + package a.b.c + + pi > 3 + ` + + nonVarName := ` + package a.b.c + + "pi" = 3 + ` + + ungroundValue := ` + package a.b.c + + pi = [3, 1, 4, x, y, z] + ` + + assertParseError(t, "multiple expressions", multipleExprs) + assertParseError(t, "non-equality", nonEquality) + assertParseError(t, "non-var name", nonVarName) + assertParseError(t, "unground value", ungroundValue) +} + +func assertParse(t *testing.T, msg string, input string, correct func([]interface{})) { + p, err := ParseStatements(input) + if err != nil { + t.Errorf("Error on test %s: parse error on %s: %s", msg, input, err) + return + } + correct(p) +} + +// TODO(tsandall): add assertions to check that error message is as expected +func assertParseError(t *testing.T, msg string, input string) { + p, err := ParseStatement(input) + if err == nil { + t.Errorf("Error on test %s: expected parse error: %v (parsed)", msg, p) + return + } +} + +func assertParseImport(t *testing.T, msg string, input string, correct *Import) { + assertParseOne(t, msg, input, func(parsed interface{}) { + imp := parsed.(*Import) + if !imp.Equal(correct) { + t.Errorf("Error on test %s: imports not equal: %v (parsed), %v (correct)", msg, imp, correct) + } + }) +} + +func assertParseModule(t *testing.T, msg string, input string, correct *Module) { + + m, err := ParseModule(input) + if err != nil { + t.Errorf("Error on test %s: parse error on %s: %s", msg, input, err) + return + } + + if !m.Equal(correct) { + t.Errorf("Error on test %s: modules not equal: %v (parsed), %v (correct)", msg, m, correct) + } + +} + +func assertParsePackage(t *testing.T, msg string, input string, correct *Package) { + assertParseOne(t, msg, input, func(parsed interface{}) { + pkg := parsed.(*Package) + if !pkg.Equal(correct) { + t.Errorf("Error on test %s: packages not equal: %v (parsed), %v (correct)", msg, pkg, correct) + } + }) +} + +func assertParseOne(t *testing.T, msg string, input string, correct func(interface{})) { + p, err := ParseStatement(input) + if err != nil { + t.Errorf("Error on test %s: parse error on %s: %s", msg, input, err) + } + correct(p) +} + +func assertParseOneExpr(t *testing.T, msg string, input string, correct *Expr) { + assertParseOne(t, msg, input, func(parsed interface{}) { + body := parsed.(Body) + if len(body) != 1 { + t.Errorf("Error on test %s: parser returned multiple expressions: %v", msg, body) + return + } + expr := body[0] + if !expr.Equal(correct) { + t.Errorf("Error on test %s: expressions not equal: %v (parsed), %v (correct)", msg, expr, correct) + } + }) +} + +func assertParseOneExprNegated(t *testing.T, msg string, input string, correct *Expr) { + correct.Negated = true + assertParseOneExpr(t, msg, input, correct) +} + +func assertParseOneTerm(t *testing.T, msg string, input string, correct *Term) { + assertParseOneExpr(t, msg, input, &Expr{Terms: correct}) +} + +func assertParseOneTermNegated(t *testing.T, msg string, input string, correct *Term) { + assertParseOneExprNegated(t, msg, input, &Expr{Terms: correct}) +} + +func assertParseRule(t *testing.T, msg string, input string, correct *Rule) { + assertParseOne(t, msg, input, func(parsed interface{}) { + rule := parsed.(*Rule) + if !rule.Equal(correct) { + t.Errorf("Error on test %s: rules not equal: %v (parsed), %v (correct)", msg, rule, correct) + } + }) +} diff --git a/opalog/policy.go b/opalog/policy.go new file mode 100644 index 0000000000..1da68ab429 --- /dev/null +++ b/opalog/policy.go @@ -0,0 +1,239 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package opalog + +import "fmt" +import "strings" + +// DefaultRootDocument is the default root document. +// All package directives inside source files are implicitly +// prefixed with the DefaultRootDocument value. +var DefaultRootDocument = VarTerm("data") + +// Keywords is an array of reserved keywords in the language. +// These are reserved names that cannot be used for variables. +var Keywords = [...]string{ + "package", "import", "not", +} + +type ( + // Module represents a collection of policies (defined by rules) + // within a namespace (defined by the package) and optional + // dependencies on external documents (defined by imports). + Module struct { + Package *Package + Imports []*Import + Rules []*Rule + } + + // Package represents the namespace of the documents produced + // by rules inside the module. + Package struct { + Location *Location + Path Ref + } + + // Import represents a dependency on a document outside of the policy + // namespace. Imports are optional. + Import struct { + Location *Location + Path Value + Alias Var + } + + // Rule represents a rule as defined by Opalog. Rules define the + // content of documents that represent policy decisions. + Rule struct { + Location *Location + Name Var + Key *Term + Value *Term + Body Body + } + + // Body represents one or more expressios contained inside a rule. + Body []*Expr + + // Expr represents a single expression contained inside the body of a rule. + Expr struct { + Location *Location + Negated bool + Terms interface{} + } +) + +// Equal returns true if this Module equals the other Module. +// Two modules are equal if they contain the same package, +// ordered imports, and ordered rules. +func (mod *Module) Equal(other *Module) bool { + if !mod.Package.Equal(other.Package) { + return false + } + if len(mod.Imports) != len(other.Imports) { + return false + } + for i := range mod.Imports { + if !mod.Imports[i].Equal(other.Imports[i]) { + return false + } + } + if len(mod.Rules) != len(other.Rules) { + return false + } + for i := range mod.Rules { + if !mod.Rules[i].Equal(other.Rules[i]) { + return false + } + } + return true +} + +// Equal returns true if this Package has the same path as the other Package. +func (pkg *Package) Equal(other *Package) bool { + return pkg.Path.Equal(other.Path) +} + +func (pkg *Package) String() string { + return fmt.Sprintf("package %v", pkg.Path) +} + +// Equal returns true if this Import has the same path and alias as the other Import. +func (imp *Import) Equal(other *Import) bool { + return imp.Alias.Equal(other.Alias) && imp.Path.Equal(other.Path) +} + +func (imp *Import) String() string { + buf := []string{"import", imp.Path.String()} + if len(imp.Alias) > 0 { + buf = append(buf, "as "+imp.Alias.String()) + } + return strings.Join(buf, " ") +} + +// Equal returns true if this Rule has the same name, arguments, and body as the other Rule. +func (rule *Rule) Equal(other *Rule) bool { + if !rule.Name.Equal(other.Name) { + return false + } + if !rule.Key.Equal(other.Key) { + return false + } + if !rule.Value.Equal(other.Value) { + return false + } + return rule.Body.Equal(other.Body) +} + +func (rule *Rule) String() string { + var buf []string + if rule.Key != nil { + buf = append(buf, rule.Name.String()+"["+rule.Key.String()+"]") + } else { + buf = append(buf, rule.Name.String()) + } + if rule.Value != nil { + buf = append(buf, "=") + buf = append(buf, rule.Value.String()) + } + if len(rule.Body) >= 0 { + buf = append(buf, ":-") + buf = append(buf, rule.Body.String()) + } + return strings.Join(buf, " ") +} + +// Equal returns true if this Body is equal to the other Body. +// Two bodies are equal if consist of equal, ordered expressions. +func (body Body) Equal(other Body) bool { + if len(body) != len(other) { + return false + } + for i := range body { + if !body[i].Equal(other[i]) { + return false + } + } + return true +} + +func (body Body) String() string { + var buf []string + for _, v := range body { + buf = append(buf, v.String()) + } + return strings.Join(buf, ", ") +} + +// Equal returns true if this Expr equals the other Expr. +// Two expressions are considered equal if both expressions are negated (or not), +// are built-ins (or not), and have the same ordered terms. +func (expr *Expr) Equal(other *Expr) bool { + if expr.Negated != other.Negated { + return false + } + switch t := expr.Terms.(type) { + case *Term: + switch u := other.Terms.(type) { + case *Term: + return t.Equal(u) + } + case []*Term: + switch u := other.Terms.(type) { + case []*Term: + return termSliceEqual(t, u) + } + } + return false +} + +// IsEquality returns true if this is an equality expression. +func (expr *Expr) IsEquality() bool { + terms, ok := expr.Terms.([]*Term) + if !ok { + return false + } + if len(terms) != 3 { + return false + } + return terms[0].Equal(VarTerm("=")) +} + +var builtinNames = map[string]string{ + "=": "eq", + "<": "lt", + ">": "gt", + "<=": "lte", + ">=": "gte", + "!=": "ne", +} + +func (expr *Expr) String() string { + var buf []string + if expr.Negated { + buf = append(buf, "not") + } + switch t := expr.Terms.(type) { + case []*Term: + var args []string + for _, v := range t[1:] { + args = append(args, v.String()) + } + name, ok := builtinNames[string(t[0].Value.(Var))] + if !ok { + name = t[0].String() + } + builtinStr := fmt.Sprintf("%s(%s)", name, strings.Join(args, ", ")) + buf = append(buf, builtinStr) + case *Term: + buf = append(buf, t.String()) + } + return strings.Join(buf, " ") +} + +// NewBuiltinExpr creates a new Expr object with the supplied terms. +// The builtin operator must be the first term. +func NewBuiltinExpr(terms ...*Term) *Expr { + return &Expr{Terms: terms} +} diff --git a/opalog/policy_test.go b/opalog/policy_test.go new file mode 100644 index 0000000000..5868079802 --- /dev/null +++ b/opalog/policy_test.go @@ -0,0 +1,272 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package opalog + +import "testing" + +func TestPackageEquals(t *testing.T) { + pkg1 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)} + pkg2 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)} + pkg3 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("qux"), StringTerm("baz")).Value.(Ref)} + assertPackagesEqual(t, pkg1, pkg1) + assertPackagesEqual(t, pkg1, pkg2) + assertPackagesNotEqual(t, pkg1, pkg3) + assertPackagesNotEqual(t, pkg2, pkg3) +} + +func TestPackageString(t *testing.T) { + pkg1 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)} + result1 := pkg1.String() + expected1 := "package foo.bar.baz" + if result1 != expected1 { + t.Errorf("Expected %v but got %v", expected1, result1) + } +} + +func TestImportEquals(t *testing.T) { + imp1 := &Import{Path: Var("foo"), Alias: Var("bar")} + imp11 := &Import{Path: Var("foo"), Alias: Var("bar")} + imp2 := &Import{Path: Var("foo")} + imp3 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")).Value, Alias: Var("corge")} + imp33 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")).Value, Alias: Var("corge")} + imp4 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")).Value} + assertImportsEqual(t, imp1, imp1) + assertImportsEqual(t, imp1, imp11) + assertImportsEqual(t, imp3, imp3) + assertImportsEqual(t, imp3, imp33) + imps := []*Import{imp1, imp2, imp3, imp4} + for i := range imps { + for j := range imps { + if i != j { + assertImportsNotEqual(t, imps[i], imps[j]) + } + } + } +} + +func TestImportString(t *testing.T) { + imp1 := &Import{Path: Var("foo"), Alias: Var("bar")} + imp2 := &Import{Path: Var("foo")} + imp3 := &Import{Path: RefTerm(VarTerm("bar"), StringTerm("baz"), StringTerm("qux")).Value, Alias: Var("corge")} + imp4 := &Import{Path: RefTerm(VarTerm("bar"), StringTerm("baz"), StringTerm("qux")).Value} + assertImportToString(t, imp1, "import foo as bar") + assertImportToString(t, imp2, "import foo") + assertImportToString(t, imp3, "import bar.baz.qux as corge") + assertImportToString(t, imp4, "import bar.baz.qux") +} + +func TestExprEquals(t *testing.T) { + + // Scalars + expr1 := &Expr{Terms: BooleanTerm(true)} + expr2 := &Expr{Terms: BooleanTerm(true)} + expr3 := &Expr{Terms: StringTerm("true")} + assertExprEqual(t, expr1, expr2) + assertExprNotEqual(t, expr1, expr3) + + // Vars, refs, and composites + ref1 := RefTerm(VarTerm("foo"), StringTerm("bar"), VarTerm("i")) + ref2 := RefTerm(VarTerm("foo"), StringTerm("bar"), VarTerm("i")) + obj1 := ObjectTerm(Item(ref1, ArrayTerm(NumberTerm(1), NullTerm()))) + obj2 := ObjectTerm(Item(ref2, ArrayTerm(NumberTerm(1), NullTerm()))) + obj3 := ObjectTerm(Item(ref2, ArrayTerm(StringTerm("1"), NullTerm()))) + expr10 := &Expr{Terms: obj1} + expr11 := &Expr{Terms: obj2} + expr12 := &Expr{Terms: obj3} + assertExprEqual(t, expr10, expr11) + assertExprNotEqual(t, expr10, expr12) + + // Builtins and negation + expr20 := &Expr{ + Negated: true, + Terms: []*Term{VarTerm("="), VarTerm("x"), ref1}, + } + expr21 := &Expr{ + Negated: true, + Terms: []*Term{VarTerm("="), VarTerm("x"), ref1}, + } + expr22 := &Expr{ + Negated: false, + Terms: []*Term{VarTerm("="), VarTerm("x"), ref1}, + } + expr23 := &Expr{ + Negated: true, + Terms: []*Term{VarTerm("="), VarTerm("y"), ref1}, + } + assertExprEqual(t, expr20, expr21) + assertExprNotEqual(t, expr20, expr22) + assertExprNotEqual(t, expr20, expr23) +} + +func TextExprString(t *testing.T) { + expr1 := &Expr{ + Terms: RefTerm(VarTerm("q"), StringTerm("r"), VarTerm("x")), + } + expr2 := &Expr{ + Negated: true, + Terms: RefTerm(VarTerm("q"), StringTerm("r"), VarTerm("x")), + } + expr3 := &Expr{ + Terms: []*Term{VarTerm("="), StringTerm("a"), NumberTerm(17.1)}, + } + expr4 := &Expr{ + Terms: []*Term{ + VarTerm("!="), + ObjectTerm(Item(VarTerm("foo"), ArrayTerm( + NumberTerm(1), RefTerm(VarTerm("a"), StringTerm("b")), + ))), + BooleanTerm(false), + }, + } + assertExprString(t, expr1, "q.r[x]") + assertExprString(t, expr2, "not q.r[x]") + assertExprString(t, expr3, "eq(\"a\", 17.1)") + assertExprString(t, expr4, "ne({foo: [1, a.b]}, false)") +} + +func TestRuleHeadEquals(t *testing.T) { + assertRulesEqual(t, &Rule{}, &Rule{}) + + // Same name/key/value + assertRulesEqual(t, &Rule{Name: Var("p")}, &Rule{Name: Var("p")}) + assertRulesEqual(t, &Rule{Key: VarTerm("x")}, &Rule{Key: VarTerm("x")}) + assertRulesEqual(t, &Rule{Value: VarTerm("x")}, &Rule{Value: VarTerm("x")}) + + // Different name/key/value + assertRulesNotEqual(t, &Rule{Name: Var("p")}, &Rule{Name: Var("q")}) + assertRulesNotEqual(t, &Rule{Key: VarTerm("x")}, &Rule{Key: VarTerm("y")}) + assertRulesNotEqual(t, &Rule{Value: VarTerm("x")}, &Rule{Value: VarTerm("y")}) +} + +func TestRuleBodyEquals(t *testing.T) { + + true1 := &Expr{Terms: []*Term{BooleanTerm(true)}} + true2 := &Expr{Terms: []*Term{BooleanTerm(true)}} + false1 := &Expr{Terms: []*Term{BooleanTerm(false)}} + + ruleTrue1 := &Rule{Body: []*Expr{true1}} + ruleTrue12 := &Rule{Body: []*Expr{true1, true2}} + ruleTrue2 := &Rule{Body: []*Expr{true2}} + ruleTrue12_2 := &Rule{Body: []*Expr{true1, true2}} + ruleFalse1 := &Rule{Body: []*Expr{false1}} + ruleTrueFalse := &Rule{Body: []*Expr{true1, false1}} + ruleFalseTrue := &Rule{Body: []*Expr{false1, true1}} + + // Same expressions + assertRulesEqual(t, ruleTrue1, ruleTrue2) + assertRulesEqual(t, ruleTrue12, ruleTrue12_2) + + // Different expressions/different order + assertRulesNotEqual(t, ruleTrue1, ruleFalse1) + assertRulesNotEqual(t, ruleTrueFalse, ruleFalseTrue) +} + +func TestRuleString(t *testing.T) { + + rule1 := &Rule{ + Name: Var("p"), + Body: []*Expr{ + &Expr{ + Terms: []*Term{ + VarTerm("="), StringTerm("foo"), StringTerm("bar"), + }, + }, + }, + } + + rule2 := &Rule{ + Name: Var("p"), + Key: VarTerm("x"), + Value: VarTerm("y"), + Body: []*Expr{ + &Expr{ + Terms: []*Term{ + VarTerm("="), StringTerm("foo"), VarTerm("x"), + }, + }, + &Expr{ + Negated: true, + Terms: RefTerm(VarTerm("a"), StringTerm("b"), VarTerm("x")), + }, + &Expr{ + Terms: []*Term{ + VarTerm("="), StringTerm("b"), VarTerm("y"), + }, + }, + }, + } + + assertRuleString(t, rule1, "p :- eq(\"foo\", \"bar\")") + assertRuleString(t, rule2, "p[x] = y :- eq(\"foo\", x), not a.b[x], eq(\"b\", y)") +} + +func assertExprEqual(t *testing.T, a, b *Expr) { + if !a.Equal(b) { + t.Errorf("Expressions are not equal (expected equal): a=%v b=%v", a, b) + } +} + +func assertExprNotEqual(t *testing.T, a, b *Expr) { + if a.Equal(b) { + t.Errorf("Expressions are equal (expected not equal): a=%v b=%v", a, b) + } +} + +func assertExprString(t *testing.T, expr *Expr, expected string) { + result := expr.String() + if result != expected { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func assertImportsEqual(t *testing.T, a, b *Import) { + if !a.Equal(b) { + t.Errorf("Imports are not equal (expected equal): a=%v b=%v", a, b) + } +} + +func assertImportsNotEqual(t *testing.T, a, b *Import) { + if a.Equal(b) { + t.Errorf("Imports are equal (expected not equal): a=%v b=%v", a, b) + } +} + +func assertImportToString(t *testing.T, imp *Import, expected string) { + result := imp.String() + if result != expected { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func assertPackagesEqual(t *testing.T, a, b *Package) { + if !a.Equal(b) { + t.Errorf("Packages are not equal (expected equal): a=%v b=%v", a, b) + } +} + +func assertPackagesNotEqual(t *testing.T, a, b *Package) { + if a.Equal(b) { + t.Errorf("Packages are not equal (expected not equal): a=%v b=%v", a, b) + } +} + +func assertRulesEqual(t *testing.T, a, b *Rule) { + if !a.Equal(b) { + t.Errorf("Rules are not equal (expected equal): a=%v b=%v", a, b) + } +} + +func assertRulesNotEqual(t *testing.T, a, b *Rule) { + if a.Equal(b) { + t.Errorf("Rules are equal (expected not equal): a=%v b=%v", a, b) + } +} + +func assertRuleString(t *testing.T, rule *Rule, expected string) { + result := rule.String() + if result != expected { + t.Errorf("Expected %v but got %v", expected, result) + } +} diff --git a/opalog/term.go b/opalog/term.go index e367259765..a84a214e41 100644 --- a/opalog/term.go +++ b/opalog/term.go @@ -34,6 +34,9 @@ type Value interface { // Equal returns true if this value equals the other value. Equal(other Value) bool + // IsGround returns true if this value is not a variable or contains no variables. + IsGround() bool + // String returns a human readable string representation of the value. String() string } @@ -47,12 +50,23 @@ type Term struct { // Equal returns true if this term equals the other term. Equality is // defined for each kind of term. func (term *Term) Equal(other *Term) bool { + if term == nil && other != nil { + return false + } + if term != nil && other == nil { + return false + } if term == other { return true } return term.Value.Equal(other.Value) } +// IsGround returns true if this terms' Value is ground. +func (term *Term) IsGround() bool { + return term.Value.IsGround() +} + func (term *Term) String() string { return term.Value.String() } @@ -65,11 +79,6 @@ func NullTerm() *Term { return &Term{Value: Null{}} } -// NullTermWithLoc creates a new Term with a Null value and a Location. -func NullTermWithLoc(loc *Location) *Term { - return &Term{Value: Null{}, Location: loc} -} - // Equal returns true if the other term Value is also Null. func (null Null) Equal(other Value) bool { switch other.(type) { @@ -80,6 +89,11 @@ func (null Null) Equal(other Value) bool { } } +// IsGround always returns true. +func (null Null) IsGround() bool { + return true +} + func (null Null) String() string { return "null" } @@ -92,11 +106,6 @@ func BooleanTerm(b bool) *Term { return &Term{Value: Boolean(b)} } -// BooleanTermWithLoc creates a new Term with a Boolean value and a Location. -func BooleanTermWithLoc(b bool, loc *Location) *Term { - return &Term{Value: Boolean(b), Location: loc} -} - // Equal returns true if the other Value is a Boolean and is equal. func (bol Boolean) Equal(other Value) bool { switch other := other.(type) { @@ -107,6 +116,11 @@ func (bol Boolean) Equal(other Value) bool { } } +// IsGround always returns true. +func (bol Boolean) IsGround() bool { + return true +} + func (bol Boolean) String() string { return strconv.FormatBool(bool(bol)) } @@ -119,11 +133,6 @@ func NumberTerm(n float64) *Term { return &Term{Value: Number(n)} } -// NumberTermWithLoc creates a new Term with a Number value and a Location. -func NumberTermWithLoc(n float64, loc *Location) *Term { - return &Term{Value: Number(n), Location: loc} -} - // Equal returns true if the other Value is a Number and is equal. func (num Number) Equal(other Value) bool { switch other := other.(type) { @@ -134,6 +143,11 @@ func (num Number) Equal(other Value) bool { } } +// IsGround always returns true. +func (num Number) IsGround() bool { + return true +} + func (num Number) String() string { return strconv.FormatFloat(float64(num), 'G', -1, 64) } @@ -146,11 +160,6 @@ func StringTerm(s string) *Term { return &Term{Value: String(s)} } -// StringTermWithLoc creates a new Term with a String value and a Location. -func StringTermWithLoc(s string, loc *Location) *Term { - return &Term{Value: String(s), Location: loc} -} - // Equal returns true if the other Value is a String and is equal. func (str String) Equal(other Value) bool { switch other := other.(type) { @@ -161,6 +170,11 @@ func (str String) Equal(other Value) bool { } } +// IsGround always returns true. +func (str String) IsGround() bool { + return true +} + func (str String) String() string { return strconv.Quote(string(str)) } @@ -173,11 +187,6 @@ func VarTerm(v string) *Term { return &Term{Value: Var(v)} } -// VarTermWithLoc creates a new Term with a Variable value and a Location. -func VarTermWithLoc(v string, loc *Location) *Term { - return &Term{Value: Var(v), Location: loc} -} - // Equal returns true if the other Value is a Variable and has the same value // (name). func (variable Var) Equal(other Value) bool { @@ -189,11 +198,16 @@ func (variable Var) Equal(other Value) bool { } } +// IsGround always returns false. +func (variable Var) IsGround() bool { + return false +} + func (variable Var) String() string { return string(variable) } -// Ref represents a variable as defined by Opalog. +// Ref represents a reference as defined by Opalog. type Ref []*Term // RefTerm creates a new Term with a Ref value. @@ -201,29 +215,22 @@ func RefTerm(r ...*Term) *Term { return &Term{Value: Ref(r)} } -// RefTermWithLoc creates a new Term with a Ref value and a Location. -func RefTermWithLoc(r []*Term, loc *Location) *Term { - return &Term{Value: Ref(r), Location: loc} -} - // Equal returns true if the other Value is a Ref and the elements of the // other Ref are equal to the this Ref. func (ref Ref) Equal(other Value) bool { switch other := other.(type) { case Ref: - if len(ref) == len(other) { - for i := range ref { - if !ref[i].Equal(other[i]) { - return false - } - } - return true - } + return termSliceEqual(ref, other) } return false } -var varRegexp = regexp.MustCompile("[[:alpha:]_][[:alpha:][:digit:]_]+") +// IsGround returns true if all of the parts of the Ref are ground. +func (ref Ref) IsGround() bool { + return termSliceIsGround(ref) +} + +var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$") func (ref Ref) String() string { buf := []string{string(ref[0].Value.(Var))} @@ -253,29 +260,22 @@ func ArrayTerm(a ...*Term) *Term { return &Term{Value: Array(a)} } -// ArrayTermWithLoc creates a new Term with an Array value and a Location. -func ArrayTermWithLoc(a []*Term, loc *Location) *Term { - return &Term{Value: Array(a), Location: loc} -} - // Equal returns true if the other Value is an Array and the elements of the // other Array are equal to the elements of this Array. The elements are // ordered. func (arr Array) Equal(other Value) bool { switch other := other.(type) { case Array: - if len(arr) == len(other) { - for i := range arr { - if !arr[i].Equal(other[i]) { - return false - } - } - return true - } + return termSliceEqual(arr, other) } return false } +// IsGround returns true if all of the Array elements are ground. +func (arr Array) IsGround() bool { + return termSliceIsGround(arr) +} + func (arr Array) String() string { var buf []string for _, e := range arr { @@ -300,11 +300,6 @@ func ObjectTerm(o ...[2]*Term) *Term { return &Term{Value: Object(o)} } -// ObjectTermWithLoc creates a new Term with an Object value and a Location. -func ObjectTermWithLoc(o [][2]*Term, loc *Location) *Term { - return &Term{Value: Object(o), Location: loc} -} - // Equal returns true if the other Value is an Object and the key/value pairs // of the Other object are equal to the key/value pairs of this Object. The // key/value pairs are ordered. @@ -326,6 +321,19 @@ func (obj Object) Equal(other Value) bool { return false } +// IsGround returns true if all of the Object key/value pairs are ground. +func (obj Object) IsGround() bool { + for i := range obj { + if !obj[i][0].IsGround() { + return false + } + if !obj[i][1].IsGround() { + return false + } + } + return true +} + func (obj Object) String() string { var buf []string for _, p := range obj { @@ -333,3 +341,24 @@ func (obj Object) String() string { } return "{" + strings.Join(buf, ", ") + "}" } + +func termSliceEqual(a, b []*Term) bool { + if len(a) == len(b) { + for i := range a { + if !a[i].Equal(b[i]) { + return false + } + } + return true + } + return false +} + +func termSliceIsGround(a []*Term) bool { + for _, v := range a { + if !v.IsGround() { + return false + } + } + return true +} diff --git a/opalog/term_test.go b/opalog/term_test.go index 79e338632b..3c5b2dd2d9 100644 --- a/opalog/term_test.go +++ b/opalog/term_test.go @@ -51,6 +51,18 @@ func TestTermsToString(t *testing.T) { assertToString(t, ArrayTerm(ObjectTerm(Item(VarTerm("foo"), ArrayTerm(RefTerm(VarTerm("bar"), VarTerm("i"))))), StringTerm("foo"), BooleanTerm(true), NullTerm(), NumberTerm(42.1)).Value, "[{foo: [bar[i]]}, \"foo\", true, null, 42.1]") } +func assertTermEqual(t *testing.T, x *Term, y *Term) { + if !x.Equal(y) { + t.Errorf("Failure on equality: \n%s and \n%s\n", x, y) + } +} + +func assertTermNotEqual(t *testing.T, x *Term, y *Term) { + if x.Equal(y) { + t.Errorf("Failure on non-equality: \n%s and \n%s\n", x, y) + } +} + func assertToString(t *testing.T, val Value, expected string) { result := val.String() if result != expected {