Merge pull request #24 from tsandall/initial-grammar

Flesh out the grammar
This commit is contained in:
Torin Sandall
2016-04-07 16:58:53 -07:00
8 changed files with 1334 additions and 143 deletions
+4 -1
View File
@@ -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
+202 -36
View File
@@ -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 <- !.
+135
View File
@@ -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 <var> = <ground term> can be
// converted into rules of the form <var> = <ground term> :- 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")
}
}
+383 -48
View File
@@ -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)
}
})
}
+239
View File
@@ -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}
}
+272
View File
@@ -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)
}
}
+87 -58
View File
@@ -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
}
+12
View File
@@ -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 {