Merge pull request #47 from tsandall/comprehensions

Comprehensions
This commit is contained in:
Torin Sandall
2016-06-21 09:26:36 -07:00
committed by GitHub
23 changed files with 1882 additions and 466 deletions
+92 -24
View File
@@ -21,20 +21,31 @@ func RegisterBuiltin(b *Builtin) {
var DefaultBuiltins = [...]*Builtin{
Equality,
GreaterThan, GreaterThanEq, LessThan, LessThanEq, NotEqual,
Plus, Minus, Multiply, Divide, Round,
Count, Sum,
ToNumber,
}
// BuiltinMap provides a convenient mapping of built-in names to
// built-in definitions.
var BuiltinMap map[Var]*Builtin
/**
* Unification
*/
// Equality represents the "=" operator.
var Equality = &Builtin{
Name: Var("="),
Alias: Var("eq"),
NumArgs: 2,
RecTargetPos: []int{0, 1},
Name: Var("="),
Alias: Var("eq"),
NumArgs: 2,
TargetPos: []int{0, 1},
}
/**
* Comparisons
*/
// GreaterThan represents the ">" comparison operator.
var GreaterThan = &Builtin{
Name: Var(">"),
@@ -70,14 +81,83 @@ var NotEqual = &Builtin{
NumArgs: 2,
}
/**
* Arithmetic
*/
// Plus adds two numbers together.
var Plus = &Builtin{
Name: Var("plus"),
NumArgs: 3,
TargetPos: []int{2},
}
// Minus subtracts the second number from the first number.
var Minus = &Builtin{
Name: Var("minus"),
NumArgs: 3,
TargetPos: []int{2},
}
// Multiply multiplies two numbers together.
var Multiply = &Builtin{
Name: Var("mul"),
NumArgs: 3,
TargetPos: []int{2},
}
// Divide divides the first number by the second number.
var Divide = &Builtin{
Name: Var("div"),
NumArgs: 3,
TargetPos: []int{2},
}
// Round rounds the number up to the nearest integer.
var Round = &Builtin{
Name: Var("round"),
NumArgs: 2,
TargetPos: []int{1},
}
/**
* Aggregates
*/
// Count takes a collection and counts the number of elements in it.
var Count = &Builtin{
Name: Var("count"),
NumArgs: 2,
TargetPos: []int{1},
}
// Sum takes an array of numbers and sums them.
var Sum = &Builtin{
Name: Var("sum"),
NumArgs: 2,
TargetPos: []int{1},
}
/**
* Casting
*/
// ToNumber takes a string, bool, or number value and converts it to a number.
// Strings are converted to numbers using strconv.Atoi.
// Boolean false is converted to 0 and boolean true is converted to 1.
var ToNumber = &Builtin{
Name: Var("to_number"),
NumArgs: 2,
TargetPos: []int{1},
}
// Builtin represents a built-in function supported by OPA. Every
// built-in function is uniquely identified by a name.
type Builtin struct {
Name Var
Alias Var
NumArgs int
TargetPos []int
RecTargetPos []int
Name Var
Alias Var
NumArgs int
TargetPos []int
}
// GetPrintableName returns a printable name for the builtin.
@@ -91,26 +171,14 @@ func (b *Builtin) GetPrintableName() string {
return b.Name.String()
}
// Unifies returns true if a term in the given position will unify
// non-recursively or recursively.
func (b *Builtin) Unifies(i int) bool {
// IsTargetPos returns true if a variable in the i-th position will be
// bound when the expression is evaluated.
func (b *Builtin) IsTargetPos(i int) bool {
for _, x := range b.TargetPos {
if x == i {
return true
}
}
return b.UnifiesRecursively(i)
}
// UnifiesRecursively returns true if a term in the given position will
// unify recursively, i.e., variables embedded inside a collection type
// will unify.
func (b *Builtin) UnifiesRecursively(i int) bool {
for _, x := range b.RecTargetPos {
if x == i {
return true
}
}
return false
}
+3 -18
View File
@@ -10,26 +10,11 @@ import (
)
func TestUnifies(t *testing.T) {
b := &Builtin{Name: Var("dummy"), NumArgs: 4, RecTargetPos: []int{2, 3}, TargetPos: []int{1}}
expected := []int{1, 2, 3}
b := &Builtin{Name: Var("dummy"), NumArgs: 4, TargetPos: []int{1, 3}}
expected := []int{1, 3}
result := []int{}
for i := 0; i < 4; i++ {
if b.Unifies(i) {
result = append(result, i)
}
}
if !reflect.DeepEqual(expected, result) {
t.Errorf("Expected %v but got: %v", expected, result)
}
}
func TestUnifiesRecursively(t *testing.T) {
b := &Builtin{Name: Var("dummy"), NumArgs: 4, RecTargetPos: []int{2, 3}, TargetPos: []int{1}}
expected := []int{2, 3}
result := []int{}
for i := 0; i < 4; i++ {
if b.UnifiesRecursively(i) {
if b.IsTargetPos(i) {
result = append(result, i)
}
}
+176 -43
View File
@@ -204,7 +204,7 @@ func (c *Compiler) checkSafetyHead() {
for _, m := range c.Modules {
for _, r := range m.Rules {
headVars := r.HeadVars()
bodyVars := r.Body.Vars()
bodyVars := r.Body.Vars(true)
for headVar := range headVars {
if _, ok := bodyVars[headVar]; !ok {
c.err("unsafe variable from head of %v: %v", r.Name, headVar)
@@ -231,16 +231,7 @@ func (c *Compiler) err(f string, a ...interface{}) {
func (c *Compiler) resolveAllRefs() {
for _, mod := range c.Modules {
for _, rule := range mod.Rules {
for _, expr := range rule.Body {
switch ts := expr.Terms.(type) {
case *Term:
expr.Terms = c.resolveRefs(c.Globals[mod], ts)
case []*Term:
for i, t := range ts {
ts[i] = c.resolveRefs(c.Globals[mod], t)
}
}
}
rule.Body = c.resolveRefsInBody(c.Globals[mod], rule.Body)
}
for i := range mod.Imports {
mod.Imports[i].Alias = Var("")
@@ -287,7 +278,30 @@ func (c *Compiler) resolveRef(globals map[Var]Value, ref Ref) Ref {
return fqn
}
func (c *Compiler) resolveRefs(globals map[Var]Value, term *Term) *Term {
func (c *Compiler) resolveRefsInBody(globals map[Var]Value, body Body) Body {
r := Body{}
for _, expr := range body {
r = append(r, c.resolveRefsInExpr(globals, expr))
}
return r
}
func (c *Compiler) resolveRefsInExpr(globals map[Var]Value, expr *Expr) *Expr {
cpy := *expr
switch ts := expr.Terms.(type) {
case *Term:
cpy.Terms = c.resolveRefsInTerm(globals, ts)
case []*Term:
buf := []*Term{}
for _, t := range ts {
buf = append(buf, c.resolveRefsInTerm(globals, t))
}
cpy.Terms = buf
}
return &cpy
}
func (c *Compiler) resolveRefsInTerm(globals map[Var]Value, term *Term) *Term {
switch v := term.Value.(type) {
case Var:
if r, ok := globals[v]; ok {
@@ -304,8 +318,8 @@ func (c *Compiler) resolveRefs(globals map[Var]Value, term *Term) *Term {
case Object:
o := Object{}
for _, i := range v {
k := c.resolveRefs(globals, i[0])
v := c.resolveRefs(globals, i[1])
k := c.resolveRefsInTerm(globals, i[0])
v := c.resolveRefsInTerm(globals, i[1])
o = append(o, Item(k, v))
}
cpy := *term
@@ -314,12 +328,19 @@ func (c *Compiler) resolveRefs(globals map[Var]Value, term *Term) *Term {
case Array:
a := Array{}
for _, e := range v {
x := c.resolveRefs(globals, e)
x := c.resolveRefsInTerm(globals, e)
a = append(a, x)
}
cpy := *term
cpy.Value = a
return &cpy
case *ArrayComprehension:
ac := &ArrayComprehension{}
ac.Term = c.resolveRefsInTerm(globals, v.Term)
ac.Body = c.resolveRefsInBody(globals, v.Body)
cpy := *term
cpy.Value = ac
return &cpy
default:
return term
}
@@ -533,6 +554,19 @@ func (vs unsafeVars) Add(e *Expr, v Var) {
}
}
func (vs unsafeVars) Set(e *Expr, s VarSet) {
vs[e] = s
}
func (vs unsafeVars) Update(o unsafeVars) {
for k, v := range o {
if _, ok := vs[k]; !ok {
vs[k] = VarSet{}
}
vs[k].Update(v)
}
}
func (vs unsafeVars) Vars() VarSet {
r := VarSet{}
for _, s := range vs {
@@ -604,50 +638,149 @@ func findRulesRec(node *ModuleTreeNode, ref Ref) []*Rule {
// contains a mapping of expressions to unsafe variables in those expressions.
func reorderBodyForSafety(globals VarSet, body Body) (Body, unsafeVars) {
body, unsafe := reorderBodyForClosures(globals, body)
if len(unsafe) != 0 {
return nil, unsafe
}
reordered := Body{}
unsafe := unsafeVars{}
safe := VarSet{}
for _, e := range body {
for v := range e.Vars() {
if !globals.Contains(v) {
for v := range e.Vars(true) {
if globals.Contains(v) {
safe.Add(v)
} else {
unsafe.Add(e, v)
}
}
}
safe := VarSet{}
for {
n := len(reordered)
for _, e := range body {
for _, e := range body {
if reordered.Contains(e) {
continue
}
safe.Update(e.OutputVars())
safe.Update(e.OutputVars(safe))
for v := range unsafe[e] {
if safe.Contains(v) {
delete(unsafe[e], v)
for v := range unsafe[e] {
if safe.Contains(v) {
delete(unsafe[e], v)
}
}
if len(unsafe[e]) == 0 {
delete(unsafe, e)
reordered = append(reordered, e)
}
}
if len(unsafe[e]) == 0 {
reordered = append(reordered, e)
delete(unsafe, e)
if len(reordered) == n {
break
}
}
// Check if other expressions in the body are considered safe
// now. If they are considered safe now, they can be added
// to the end of the re-ordered body.
for _, e := range body {
if reordered.Contains(e) {
continue
}
for v := range unsafe[e] {
if safe.Contains(v) {
delete(unsafe[e], v)
}
}
if len(unsafe[e]) == 0 {
reordered = append(reordered, e)
delete(unsafe, e)
}
// Recursively visit closures and perform the safety checks on them.
// Update the globals at each expression to include the variables that could
// be closed over.
g := globals.Copy()
for i, e := range reordered {
if i > 0 {
g.Update(reordered[i-1].Vars(true))
}
vis := &bodySafetyVisitor{
current: e,
globals: g,
unsafe: unsafe,
}
Walk(vis, e)
}
return reordered, unsafe
}
type bodySafetyVisitor struct {
current *Expr
globals VarSet
unsafe unsafeVars
}
func (vis *bodySafetyVisitor) Visit(x interface{}) Visitor {
switch x := x.(type) {
case *Expr:
cpy := *vis
cpy.current = x
return &cpy
case *ArrayComprehension:
vis.checkArrayComprehensionSafety(x)
return nil
}
return vis
}
func (vis *bodySafetyVisitor) checkArrayComprehensionSafety(ac *ArrayComprehension) {
// Check term for safety. This is analagous to the rule head safety check.
tv := ac.Term.Vars()
bv := ac.Body.Vars(true)
bv.Update(vis.globals)
uv := tv.Diff(bv)
for v := range uv {
vis.unsafe.Add(vis.current, v)
}
// Check body for safety, reordering as necessary.
r, u := reorderBodyForSafety(vis.globals, ac.Body)
if len(u) == 0 {
ac.Body = r
} else {
vis.unsafe.Update(u)
}
}
// reorderBodyForClosures returns a copy of the body ordered such that
// expressions (such as array comprehensions) that close over variables are ordered
// after other expressions that contain the same variable in an output position.
func reorderBodyForClosures(globals VarSet, body Body) (Body, unsafeVars) {
reordered := Body{}
unsafe := unsafeVars{}
for {
n := len(reordered)
for _, e := range body {
if reordered.Contains(e) {
continue
}
// Collect vars that are contained in closures within this
// expression.
vs := VarSet{}
WalkClosures(e, func(x interface{}) bool {
vis := &varVisitor{vars: vs}
Walk(vis, x)
return true
})
// Compute vars that are closed over from the body but not yet
// contained in the output position of an expression in the reordered
// body. These vars are considered unsafe.
cv := vs.Intersect(body.Vars(true)).Diff(globals)
uv := cv.Diff(reordered.OutputVars(globals))
if len(uv) == 0 {
reordered = append(reordered, e)
delete(unsafe, e)
} else {
unsafe.Set(e, uv)
}
}
if len(reordered) == n {
break
}
}
+263 -108
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"reflect"
"sort"
"strings"
"testing"
)
@@ -16,7 +17,7 @@ func TestModuleTree(t *testing.T) {
mods := getCompilerTestModules()
tree := NewModuleTree(mods)
if tree.Size() != 4 {
if tree.Size() != 5 {
t.Errorf("Expected size of 4 in module tree but got: %v", tree.Size())
}
@@ -119,6 +120,229 @@ func TestCompilerSetGlobals(t *testing.T) {
bar: data.bar}`)
}
func TestCompilerCheckSafetyHead(t *testing.T) {
c := NewCompiler()
c.Modules = getCompilerTestModules()
c.Modules["newMod"] = MustParseModule(`
package a.b
unboundKey[x] = y :- q[y] = {"foo": [1,2,[{"bar": y}]]}
unboundVal[y] = x :- q[y] = {"foo": [1,2,[{"bar": y}]]}
unboundCompositeVal[y] = [{"foo": x, "bar": y}] :- q[y] = {"foo": [1,2,[{"bar": y}]]}
`)
compileStages(c, "", "checkSafetyHead")
if len(c.Errors) != 3 {
t.Errorf("Expected exactly 3 errors but got: %v", c.Errors)
return
}
}
func TestCompilerCheckSafetyBodyReordering(t *testing.T) {
tests := []struct {
note string
body string
expected interface{}
}{
// trivial cases
{"noop", "x = 1, x != 0", "x = 1, x != 0"},
{"var/ref", "a[i] = x, a = [1,2,3,4]", "a = [1,2,3,4], a[i] = x"},
{"negation",
"a = [true, false], b = [true, false], not a[i], b[i]",
"a = [true, false], b = [true, false], b[i], not a[i]"},
{"built-in", "x != 0, count([1,2,3], x)", "count([1,2,3], x), x != 0"},
{"var/var 1", "x = y, z = 1, y = z", "z = 1, y = z, x = y"},
{"var/var 2", "x = y, 1 = z, z = y", "1 = z, z = y, x = y"},
{"var/var 3", "x != 0, y = x, y = 1", "y = 1, y = x, x != 0"},
// comprehensions
{"array compr/var", "x != 0, [y | y = 1] = x", "[y | y = 1] = x, x != 0"},
{"array compr/array", "[1] != [x], [y | y = 1] = [x]", "[y | y = 1] = [x], [1] != [x]"},
}
for i, tc := range tests {
c := NewCompiler()
c.Modules = map[string]*Module{
"mod": MustParseModule(
fmt.Sprintf(`package test
p :- %s`, tc.body)),
}
compileStages(c, "", "checkSafetyBody")
switch exp := tc.expected.(type) {
case string:
if c.Failed() {
t.Errorf("%v (#%d): Unexpected compilation error: %v", tc.note, i, c.FlattenErrors())
return
}
e := MustParseBody(exp)
if !e.Equal(c.Modules["mod"].Rules[0].Body) {
t.Errorf("%v (#%d): Expected body to be ordered and equal to %v but got: %v", tc.note, i, e, c.Modules["mod"].Rules[0].Body)
}
case error:
if len(c.Errors) > 0 {
if !reflect.DeepEqual(c.Errors[0], exp) {
t.Errorf("%v (#%d): Expected compiler error %v but got: %v", tc.note, i, exp, c.Errors[0])
}
} else {
t.Errorf("%v (#%d): Expected compiler error but got: %v", tc.note, i, c.Modules["mod"].Rules[0])
}
}
}
}
func TestCompilerCheckSafetyBodyReorderingClosures(t *testing.T) {
c := NewCompiler()
c.Modules = map[string]*Module{
"mod": MustParseModule(
`
package compr
import data.b
import data.c
p :- v = [null | true], # leave untouched
xs = [x | a[i] = x, a = [y | y != 1, y = c[j]]], # close over 'i' and 'j', 2-level reorder
xs[j] > 0,
b[i] = j
# test that reordering is not performed when closing over different globals, e.g.,
# built-ins, data, imports.
q :- _ = [x | x = b[i]],
_ = b[j],
_ = [x | x = true, x != false],
true != false,
_ = [x | data.foo[_] = x],
data.foo[_] = _
`),
}
compileStages(c, "", "checkSafetyBody")
assertNotFailed(t, c)
result1 := c.Modules["mod"].Rules[0].Body
expected1 := MustParseBody(`
v = [null | true],
b[i] = j,
xs = [x | a = [y | y = c[j], y != 1], a[i] = x],
xs[j] > 0
`)
if !result1.Equal(expected1) {
t.Errorf("Expected reordered body to be equal to:\n%v\nBut got:\n%v", expected1, result1)
}
result2 := c.Modules["mod"].Rules[1].Body
expected2 := MustParseBody(`
_ = [x | x = b[i]],
_ = b[j],
_ = [x | x = true, x != false],
true != false,
_ = [x | data.foo[_] = x],
data.foo[_] = _
`)
if !result2.Equal(expected2) {
t.Errorf("Expected pre-ordered body to equal:\n%v\nBut got:\n%v", expected2, result2)
}
}
func TestCompilerCheckSafetyBodyErrors(t *testing.T) {
c := NewCompiler()
c.Modules = getCompilerTestModules()
c.Modules = map[string]*Module{
"newMod": MustParseModule(`
package a.b
import a.b.c as foo
import x as bar
import data.m.n as baz
# deadbeef is not a built-interface{}
badBuiltin = true :- deadbeef(1,2,3)
# a would be unbound
unboundRef1 = true :- a.b.c = "foo"
# a would be unbound
unboundRef2 = true :- {"foo": [{"bar": a.b.c}]} = {"foo": [{"bar": "baz"}]}
# i will be bound even though it's in a non-output position
inputPosRef = true :- a = [1,2,3,4], a[i] != 100
# i and x would be unbound
unboundNegated1 = true :- a = [1,2,3,4], not a[i] = x
# i and x would be unbound even though x appears in head
unboundNegated2[x] :- a = [1,2,3,4], not a[i] = x
# x, i, and j would be unbound even though they appear in other expressions
unboundNegated3[x] = true :- a = [1,2,3,4], b = [1,2,3,4], not a[i] = x, not b[j] = x
# i and j would be unbound even though they are in embedded references
unboundNegated4 = true :- a = [{"foo": ["bar", "baz"]}], not a[0].foo = [a[0].foo[i], a[0].foo[j]]
# x would be unbound as input to count
unsafeBuiltin :- count([1,2,x], x)
# i and x would be bound in the last expression so the third expression is safe
negatedSafe = true :- a = [1,2,3,4], b = [1,2,3,4], not a[i] = x, b[i] = x
# x would be unbound because it does not appear in the target position of any expression
unboundNoTarget = true :- x > 0, x <= 3, x != 2
unboundArrayComprBody1 :- _ = [x | x = data.a[_], y > 1]
unboundArrayComprBody2 :- _ = [x | x = a[_], a = [y | y = data.a[_], z > 1]]
unboundArrayComprBody3 :- _ = [v | v = [x | x = data.a[_]], x > 1]
unboundArrayComprTerm1 :- _ = [u | true]
unboundArrayComprTerm2 :- _ = [v | v = [w | w != 0]]
unboundArrayComprTerm3 :- _ = [x[i] | x = []]
unboundArrayComprMixed1 :- _ = [x | y = [a | a = z[i]]]
unsafeClosure1 :- x = [x | x = 1]
unsafeClosure2 :- x = y, x = [y | y = 1]
negatedImport1 = true :- not foo
negatedImport2 = true :- not bar
negatedImport3 = true :- not baz
`)}
compileStages(c, "", "checkSafetyBody")
expected := []error{
fmt.Errorf("unsafe variables in badBuiltin: [deadbeef]"),
fmt.Errorf("unsafe variables in unboundRef1: [a]"),
fmt.Errorf("unsafe variables in unboundRef2: [a]"),
fmt.Errorf("unsafe variables in unboundNegated1: [i x]"),
fmt.Errorf("unsafe variables in unboundNegated2: [i x]"),
fmt.Errorf("unsafe variables in unboundNegated3: [i j x]"),
fmt.Errorf("unsafe variables in unboundNegated4: [i j]"),
fmt.Errorf("unsafe variables in unsafeBuiltin: [x]"),
fmt.Errorf("unsafe variables in unboundNoTarget: [x]"),
fmt.Errorf("unsafe variables in unboundArrayComprBody1: [y]"),
fmt.Errorf("unsafe variables in unboundArrayComprBody2: [z]"),
fmt.Errorf("unsafe variables in unboundArrayComprBody3: [x]"),
fmt.Errorf("unsafe variables in unboundArrayComprTerm1: [u]"),
fmt.Errorf("unsafe variables in unboundArrayComprTerm2: [w]"),
fmt.Errorf("unsafe variables in unboundArrayComprTerm3: [i]"),
fmt.Errorf("unsafe variables in unboundArrayComprMixed1: [x z]"),
fmt.Errorf("unsafe variables in unsafeClosure1: [x]"),
fmt.Errorf("unsafe variables in unsafeClosure2: [y]"),
}
if !reflect.DeepEqual(expected, c.Errors) {
e := []string{}
for _, x := range expected {
e = append(e, x.Error())
}
r := []string{}
for _, x := range c.Errors {
r = append(r, x.Error())
}
t.Errorf("Expected:\n%v\nBut got:\n%v", strings.Join(e, "\n"), strings.Join(r, "\n"))
}
}
func TestCompilerResolveAllRefs(t *testing.T) {
c := NewCompiler()
c.Modules = getCompilerTestModules()
@@ -158,116 +382,26 @@ func TestCompilerResolveAllRefs(t *testing.T) {
if !term.Equal(e) {
t.Errorf("Wrong term (nested refs): expected %v but got: %v", e, term)
}
}
func TestCompilerCheckSafetyHead(t *testing.T) {
c := NewCompiler()
c.Modules = getCompilerTestModules()
c.Modules["newMod"] = MustParseModule(`
package a.b
unboundKey[x] = y :- q[y] = {"foo": [1,2,[{"bar": y}]]}
unboundVal[y] = x :- q[y] = {"foo": [1,2,[{"bar": y}]]}
unboundCompositeVal[y] = [{"foo": x, "bar": y}] :- q[y] = {"foo": [1,2,[{"bar": y}]]}
`)
compileStages(c, "", "checkSafetyHead")
// Array comprehensions.
mod5 := c.Modules["mod5"]
if len(c.Errors) != 3 {
t.Errorf("Expected exactly 3 errors but got: %v", c.Errors)
return
}
}
func TestCompilerCheckSafetyBodyReordering(t *testing.T) {
c := NewCompiler()
c.Modules = getCompilerTestModules()
c.Modules["newMod"] = MustParseModule(`
package a.b
needsReorder = true :- a[i] = x, a = [1,2,3,4]
needsReorderNegated = true :- a = [true, false], b = [true, false], not a[i], b[i]
`)
compileStages(c, "", "checkSafetyBody")
assertNotFailed(t, c)
expected1 := MustParseBody(`a = [1,2,3,4], a[i] = x`)
reordered1 := c.Modules["newMod"].Rules[0].Body
if !expected1.Equal(reordered1) {
t.Errorf("Expected body to be re-ordered and equal to %v but got: %v", expected1, reordered1)
ac := func(r *Rule) *ArrayComprehension {
return r.Body[0].Terms.(*Term).Value.(*ArrayComprehension)
}
expected2 := MustParseBody(`a = [true, false], b = [true, false], b[i], not a[i]`)
reordered2 := c.Modules["newMod"].Rules[1].Body
if !expected2.Equal(reordered2) {
t.Errorf("Expected body to be re-ordered and equal to %v but got: %v", expected2, reordered2)
}
}
func TestCompilerCheckSafetyBodyErrors(t *testing.T) {
c := NewCompiler()
c.Modules = getCompilerTestModules()
c.Modules = map[string]*Module{
"newMod": MustParseModule(`
package a.b
import a.b.c as foo
import x as bar
import data.m.n as baz
# deadbeef is not a built-interface{}
badBuiltin = true :- deadbeef(1,2,3)
# a would be unbound
unboundRef1 = true :- a.b.c = "foo"
# a would be unbound
unboundRef2 = true :- {"foo": [{"bar": a.b.c}]} = {"foo": [{"bar": "baz"}]}
# i will be bound even though it's in a non-output position
inputPosRef = true :- a = [1,2,3,4], a[i] != 100
# i and x would be unbound
unboundNegated1 = true :- a = [1,2,3,4], not a[i] = x
# i and x would be unbound even though x appears in head
unboundNegated2[x] :- a = [1,2,3,4], not a[i] = x
# x, i, and j would be unbound even though they appear in other expressions
unboundNegated3[x] = true :- a = [1,2,3,4], b = [1,2,3,4], not a[i] = x, not b[j] = x
# i and j would be unbound even though they are in embedded references
unboundNegated4 = true :- a = [{"foo": ["bar", "baz"]}], not a[0].foo = [a[0].foo[i], a[0].foo[j]]
# i and x would be bound in the last expression so the third expression is safe
negatedSafe = true :- a = [1,2,3,4], b = [1,2,3,4], not a[i] = x, b[i] = x
# x would be unbound because it does not appear in the target position of any expression
unboundNoTarget = true :- x > 0, x <= 3, x != 2
negatedImport1 = true :- not foo
negatedImport2 = true :- not bar
negatedImport3 = true :- not baz
`)}
compileStages(c, "", "checkSafetyBody")
expected := []error{
fmt.Errorf("unsafe variables in badBuiltin: [deadbeef]"),
fmt.Errorf("unsafe variables in unboundRef1: [a]"),
fmt.Errorf("unsafe variables in unboundRef2: [a]"),
fmt.Errorf("unsafe variables in unboundNegated1: [i x]"),
fmt.Errorf("unsafe variables in unboundNegated2: [i x]"),
fmt.Errorf("unsafe variables in unboundNegated3: [i j x]"),
fmt.Errorf("unsafe variables in unboundNegated4: [i j]"),
fmt.Errorf("unsafe variables in unboundNoTarget: [x]"),
}
if !reflect.DeepEqual(expected, c.Errors) {
t.Errorf("Expected %v but got:%v", expected, c.Errors)
}
acTerm1 := ac(mod5.Rules[0])
assertTermEqual(t, acTerm1.Term, MustParseTerm("x.a"))
acTerm2 := ac(mod5.Rules[1])
assertTermEqual(t, acTerm2.Term, MustParseTerm("a.b.c.q.a"))
acTerm3 := ac(mod5.Rules[2])
assertTermEqual(t, acTerm3.Body[0].Terms.([]*Term)[1], MustParseTerm("x.a"))
acTerm4 := ac(mod5.Rules[3])
assertTermEqual(t, acTerm4.Body[0].Terms.([]*Term)[1], MustParseTerm("a.b.c.q[i]"))
acTerm5 := ac(mod5.Rules[4])
assertTermEqual(t, acTerm5.Body[0].Terms.([]*Term)[2].Value.(*ArrayComprehension).Term, MustParseTerm("x.a"))
acTerm6 := ac(mod5.Rules[5])
assertTermEqual(t, acTerm6.Body[0].Terms.([]*Term)[2].Value.(*ArrayComprehension).Body[0].Terms.([]*Term)[1], MustParseTerm("a.b.c.q[i]"))
}
@@ -325,6 +459,11 @@ func TestCompilerCheckRecursion(t *testing.T) {
import data.rec3.p
q[x] = y :- p[x] = y
`),
"newMod6": MustParseModule(`
package rec5
acp[x] :- acq[x]
acq[x] :- a = [x | acp[x]], a[i] = x
`),
}
compileStages(c, "", "checkRecursion")
@@ -338,6 +477,8 @@ func TestCompilerCheckRecursion(t *testing.T) {
fmt.Errorf("recursion found in e: e, a, b, c, e"),
fmt.Errorf("recursion found in p: p, q, p"),
fmt.Errorf("recursion found in q: q, p, q"),
fmt.Errorf("recursion found in acq: acq, acp, acq"),
fmt.Errorf("recursion found in acp: acp, acq, acp"),
}
if len(c.Errors) != len(expected) {
@@ -517,5 +658,19 @@ func getCompilerTestModules() map[string]*Module {
package a.b.empty
`)
return map[string]*Module{"mod2": mod2, "mod3": mod3, "mod1": mod1, "mod4": mod4}
mod5 := MustParseModule(`
package a.b.compr
import x as y
import a.b.c.q
p :- [y.a | true]
r :- [q.a | true]
s :- [true | y.a = 0]
t :- [true | q[i] = 1]
u :- [true | _ = [y.a | true]]
v :- [true | _ = [ true | q[i] = 1]]
`)
return map[string]*Module{"mod2": mod2, "mod3": mod3, "mod1": mod1, "mod4": mod4, "mod5": mod5}
}
+13
View File
@@ -166,6 +166,7 @@ func ParseStatements(input string) ([]interface{}, error) {
return nil, err
}
stmts := parsed.([]interface{})
postProcess(stmts)
return stmts, err
}
@@ -266,6 +267,18 @@ func parseModule(stmts []interface{}) (*Module, error) {
return mod, nil
}
func postProcess(stmts []interface{}) {
mangleWildcards(stmts)
}
func mangleWildcards(stmts []interface{}) {
mangler := &wildcardMangler{}
for _, stmt := range stmts {
Walk(mangler, stmt)
}
}
type wildcardMangler struct {
c int
}
+66 -1
View File
@@ -140,6 +140,47 @@ 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 TestArrayComprehensions(t *testing.T) {
input := `[
{"x": [a[i] | xs = [{"a": ["baz", j]} | q[p], p.a != "bar", j = "foo"],
xs[j].a[k] = "foo"]}
]`
expected := ArrayTerm(
ObjectTerm(Item(
StringTerm("x"),
ArrayComprehensionTerm(
RefTerm(VarTerm("a"), VarTerm("i")),
Body{
NewBuiltinExpr(
VarTerm("="),
VarTerm("xs"),
ArrayComprehensionTerm(
ObjectTerm(Item(StringTerm("a"), ArrayTerm(StringTerm("baz"), VarTerm("j")))),
Body{
&Expr{
Terms: RefTerm(VarTerm("q"), VarTerm("p")),
},
NewBuiltinExpr(VarTerm("!="), RefTerm(VarTerm("p"), StringTerm("a")), StringTerm("bar")),
NewBuiltinExpr(VarTerm("="), VarTerm("j"), StringTerm("foo")),
},
),
),
NewBuiltinExpr(
VarTerm("="),
RefTerm(VarTerm("xs"), VarTerm("j"), StringTerm("a"), VarTerm("k")),
StringTerm("foo"),
),
},
),
)),
)
assertParseOneTerm(t, "nested", input, expected)
}
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()))
@@ -295,6 +336,10 @@ func TestComments(t *testing.T) {
:- m = [1,2,
3],
a = m[i]
r[x] :- x = [ a | # inside comprehension
a = z[i],
b[i].a = a ]
`
assertParseModule(t, "module comments", testModule, &Module{
@@ -307,6 +352,7 @@ func TestComments(t *testing.T) {
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),
MustParseStatement("r[x] :- x = [a | a = z[i], b[i].a = a]").(*Rule),
},
})
}
@@ -416,6 +462,25 @@ func TestWildcards(t *testing.T) {
),
},
})
assertParseOneExpr(t, "comprehension", "_ = [x | a = a[_]]", &Expr{
Terms: []*Term{
VarTerm("="),
VarTerm("$0"),
ArrayComprehensionTerm(
VarTerm("x"),
Body{
&Expr{
Terms: []*Term{
VarTerm("="),
VarTerm("a"),
RefTerm(VarTerm("a"), VarTerm("$1")),
},
},
},
),
},
})
}
func assertParse(t *testing.T, msg string, input string, correct func([]interface{})) {
@@ -485,7 +550,7 @@ func assertParseOneExpr(t *testing.T, msg string, input string, correct *Expr) {
}
expr := body[0]
if !expr.Equal(correct) {
t.Errorf("Error on test %s: expressions not equal: %v (parsed), %v (correct)", msg, expr, correct)
t.Errorf("Error on test %s: expressions not equal:\n%v (parsed)\n%v (correct)", msg, expr, correct)
}
})
}
+165 -80
View File
@@ -219,6 +219,35 @@ func (body Body) Equal(other Body) bool {
return true
}
// Hash returns the hash code for the Body.
func (body Body) Hash() int {
s := 0
for _, e := range body {
s += e.Hash()
}
return s
}
// IsGround returns true if all of the expressions in the Body are ground.
func (body Body) IsGround() bool {
for _, e := range body {
if !e.IsGround() {
return false
}
}
return true
}
// OutputVars returns a VarSet containing the variables that would be bound by evaluating
// the body.
func (body Body) OutputVars(safe VarSet) VarSet {
o := safe.Copy()
for _, e := range body {
o.Update(e.OutputVars(o))
}
return o.Diff(safe)
}
func (body Body) String() string {
var buf []string
for _, v := range body {
@@ -227,10 +256,13 @@ func (body Body) String() string {
return strings.Join(buf, ", ")
}
// Vars returns map where keys represent all of the variables found in the
// body. The values of the map are ignored.
func (body Body) Vars() VarSet {
vis := &varVisitor{vars: VarSet{}}
// Vars returns a VarSet containing all of the variables in the body. If skipClosures is true,
// variables contained inside closures within the body will be ignored.
func (body Body) Vars(skipClosures bool) VarSet {
vis := &varVisitor{
vars: VarSet{},
skipClosures: skipClosures,
}
Walk(vis, body)
return vis.vars
}
@@ -264,6 +296,23 @@ func (expr *Expr) Equal(other *Expr) bool {
return false
}
// Hash returns the hash code of the Expr.
func (expr *Expr) Hash() int {
s := 0
switch ts := expr.Terms.(type) {
case []*Term:
for _, t := range ts {
s += t.Value.Hash()
}
case *Term:
s += ts.Value.Hash()
}
if expr.Negated {
s++
}
return s
}
// IsEquality returns true if this is an equality expression.
func (expr *Expr) IsEquality() bool {
terms, ok := expr.Terms.([]*Term)
@@ -276,46 +325,39 @@ func (expr *Expr) IsEquality() bool {
return terms[0].Equal(VarTerm("="))
}
// OutputVars returns the set of variables that would be bound by
// evaluating this expression in isolation.
func (expr *Expr) OutputVars() VarSet {
result := VarSet{}
if expr.Negated {
return result
}
vis := &varVisitor{
skipRefHead: true,
skipObjectKeys: true,
vars: VarSet{},
}
// IsGround returns true if all of the expression terms are ground.
func (expr *Expr) IsGround() bool {
switch ts := expr.Terms.(type) {
case *Term:
if r, ok := ts.Value.(Ref); ok {
Walk(vis, r)
}
case []*Term:
b := BuiltinMap[ts[0].Value.(Var)]
for i, t := range ts[1:] {
switch v := t.Value.(type) {
case Object, Array:
if b.UnifiesRecursively(i) {
Walk(vis, v)
for _, t := range ts[1:] {
if !t.IsGround() {
return false
}
}
case *Term:
return ts.IsGround()
}
return true
}
// OutputVars returns a VarSet containing variables that would be bound by evaluating
// this expression.
func (expr *Expr) OutputVars(safe VarSet) VarSet {
if !expr.Negated {
switch terms := expr.Terms.(type) {
case *Term:
return expr.outputVarsRefs()
case []*Term:
name := terms[0].Value.(Var)
if b := BuiltinMap[name]; b != nil {
if b.Name.Equal(Equality.Name) {
return expr.outputVarsEquality(safe)
}
case Var:
if b.Unifies(i) {
result.Add(v)
}
case Ref:
Walk(vis, v)
return expr.outputVarsBuiltins(b, safe)
}
}
}
result.Update(vis.vars)
return result
return VarSet{}
}
func (expr *Expr) String() string {
@@ -349,52 +391,77 @@ func (expr *Expr) UnmarshalJSON(bs []byte) error {
if err := json.Unmarshal(bs, &v); err != nil {
return err
}
n, ok := v["Negated"]
if !ok {
expr.Negated = false
} else {
b, ok := n.(bool)
if !ok {
return unmarshalError(n, "bool")
}
expr.Negated = b
}
switch ts := v["Terms"].(type) {
case map[string]interface{}:
v, err := unmarshalValue(ts)
if err != nil {
return err
}
expr.Terms = &Term{Value: v}
case []interface{}:
buf := []*Term{}
for _, v := range ts {
e, ok := v.(map[string]interface{})
if !ok {
return unmarshalError(v, "map[string]interface{}")
}
v, err := unmarshalValue(e)
if err != nil {
return err
}
buf = append(buf, &Term{Value: v})
}
expr.Terms = buf
default:
return unmarshalError(v["Terms"], "Term or []Term")
}
return nil
return unmarshalExpr(expr, v)
}
// Vars returns a VarSet containing all of the variables in the expression.
func (expr *Expr) Vars() VarSet {
vis := &varVisitor{vars: VarSet{}}
// If skipClosures is true then variables contained inside closures within this
// expression will not be included in the VarSet.
func (expr *Expr) Vars(skipClosures bool) VarSet {
vis := &varVisitor{
skipClosures: skipClosures,
vars: VarSet{},
}
Walk(vis, expr)
return vis.vars
}
func (expr *Expr) outputVarsBuiltins(b *Builtin, safe VarSet) VarSet {
o := expr.outputVarsRefs()
terms := expr.Terms.([]*Term)
// Check that all input terms are ground or safe.
for i, t := range terms[1:] {
if b.IsTargetPos(i) {
continue
}
if t.Value.IsGround() {
continue
}
vis := &varVisitor{
skipClosures: true,
skipObjectKeys: true,
skipRefHead: true,
skipBuiltinNames: true,
vars: VarSet{},
}
Walk(vis, t)
unsafe := vis.vars.Diff(o).Diff(safe)
if len(unsafe) > 0 {
return VarSet{}
}
}
// Add vars in target positions to result.
for i, t := range terms[1:] {
if v, ok := t.Value.(Var); ok {
if b.IsTargetPos(i) {
o.Add(v)
}
}
}
return o
}
func (expr *Expr) outputVarsEquality(safe VarSet) VarSet {
ts := expr.Terms.([]*Term)
o := expr.outputVarsRefs()
o.Update(safe)
o.Update(Unify(o, ts[1], ts[2]))
return o.Diff(safe)
}
func (expr *Expr) outputVarsRefs() VarSet {
o := VarSet{}
WalkRefs(expr, func(r Ref) bool {
o.Update(r.OutputVars())
return false
})
return o
}
// NewBuiltinExpr creates a new Expr object with the supplied terms.
// The builtin operator must be the first term.
func NewBuiltinExpr(terms ...*Term) *Expr {
@@ -402,9 +469,11 @@ func NewBuiltinExpr(terms ...*Term) *Expr {
}
type varVisitor struct {
skipRefHead bool
skipObjectKeys bool
vars VarSet
skipRefHead bool
skipObjectKeys bool
skipClosures bool
skipBuiltinNames bool
vars VarSet
}
func (vis *varVisitor) Visit(v interface{}) Visitor {
@@ -424,6 +493,22 @@ func (vis *varVisitor) Visit(v interface{}) Visitor {
return nil
}
}
if vis.skipClosures {
switch v.(type) {
case *ArrayComprehension:
return nil
}
}
if vis.skipBuiltinNames {
if v, ok := v.(*Expr); ok {
if ts, ok := v.Terms.([]*Term); ok {
for _, t := range ts[1:] {
Walk(vis, t)
}
return nil
}
}
}
if v, ok := v.(Var); ok {
vis.vars.Add(v)
}
+44 -9
View File
@@ -6,6 +6,7 @@ package ast
import (
"encoding/json"
"fmt"
"reflect"
"testing"
)
@@ -18,6 +19,7 @@ func TestModuleJSONRoundTrip(t *testing.T) {
p = [1,2,{"foo":3}] :- r[x] = 1, not q[x]
r[y] = v :- i[1] = y, v = i[2]
q[x] :- a=[true,false,null,{"x":[1,2,3]}], a[i] = x
t = true :- xs = [{"x": a[i].a} | a[i].n = "bob", b[x]]
`)
bs, err := json.Marshal(mod)
@@ -131,13 +133,46 @@ func TestExprEquals(t *testing.T) {
assertExprNotEqual(t, expr20, expr23)
}
func TestBodyIsGround(t *testing.T) {
if MustParseBody(`a.b[0] = 1, a = [1,2,x]`).IsGround() {
t.Errorf("Expected body to be non-ground")
}
}
func TestExprOutputVars(t *testing.T) {
body := MustParseBody(`{"a": [{x: y}, b[z]]} = c[i], [{"a": d[j][k]}] != xs`)
one := body[0]
vars := one.OutputVars()
expected := NewVarSet(Var("y"), Var("z"), Var("i"))
if !reflect.DeepEqual(expected, vars) {
t.Errorf("Expected output vars %v from %v but got: %v", expected, one, vars)
tests := []struct {
note string
expr string
safe string
expected string
}{
{"ref 1", "a[i].b[j]", "[a]", "[i, j]"},
{"ref 2", "[1,2,a[i]]", "[a]", "[i]"},
{"simple unify", `{"a": [{x: y}, b[z]]} = c[i]`, "[b, c]", "[y, z, i]"},
{"built-in", "count([], x)", "[]", "[x]"},
}
for i, tc := range tests {
expr := MustParseBody(tc.expr)[0]
safe := VarSet{}
for _, x := range MustParseTerm(tc.safe).Value.(Array) {
safe.Add(x.Value.(Var))
}
result := expr.OutputVars(safe)
expected := VarSet{}
for _, x := range MustParseTerm(tc.expected).Value.(Array) {
expected.Add(x.Value.(Var))
}
missing := expected.Diff(result)
extra := result.Diff(expected)
if len(missing) != 0 || len(extra) != 0 {
t.Errorf("%s (%d): Missing output vars: %v, extra output vars: %v", tc.note, i, missing, extra)
}
}
}
@@ -187,7 +222,7 @@ func TestExprBadJSON(t *testing.T) {
}
`
exp := unmarshalError(100.0, "bool")
exp := fmt.Errorf("ast: unable to unmarshal Negated field with type: float64 (expected true or false)")
assert(js, exp)
js = `
@@ -197,7 +232,7 @@ func TestExprBadJSON(t *testing.T) {
]
}
`
exp = unmarshalError("foo", "map[string]interface{}")
exp = fmt.Errorf("ast: unable to unmarshal term")
assert(js, exp)
js = `
@@ -205,7 +240,7 @@ func TestExprBadJSON(t *testing.T) {
"Terms": "bad value"
}
`
exp = unmarshalError("bad value", "Term or []Term")
exp = fmt.Errorf(`ast: unable to unmarshal Terms field with type: string (expected {"Value": ..., "Type": ...} or [{"Value": ..., "Type": ...}, ...])`)
assert(js, exp)
}
+9 -4
View File
@@ -90,7 +90,6 @@ Import <- "import" ws path:(Ref / Var) alias:(ws "as" ws Var)? {
return imp, nil
}
// TODO(tsandall): update to handle underscore variables
Rule <- name:Var key:( _ "[" _ Term _ "]" _ )? value:( _ "=" _ Term )? body:( _ ":-" _ Body) {
rule := &Rule{}
@@ -130,8 +129,6 @@ Body <- head:Expr tail:( _ "," _ Expr)* {
expr := s.([]interface{})[3].(*Expr)
buf = append(buf, expr)
}
mangler := &wildcardMangler{}
Walk(mangler, buf)
return buf, nil
}
@@ -169,10 +166,18 @@ PrefixExpr <- op:Var "(" _ head:Term? tail:( _ "," _ Term )* _ ")" {
return buf, nil
}
Term <- val:( Composite / Scalar / Ref / Var ) {
Term <- val:( Comprehension / Composite / Scalar / Ref / Var ) {
return val, nil
}
Comprehension <- ArrayComprehension
ArrayComprehension <- "[" _ term:Term _ "|" _ body:Body _ "]" {
ac := ArrayComprehensionTerm(term.(*Term), body.(Body))
ac.Location = currentLocation(c)
return ac, nil
}
Composite <- Object / Array
Scalar <- Number / String / Bool / Null
+184 -72
View File
@@ -34,6 +34,7 @@ func NewLocation(text []byte, file string, row int, col int) *Location {
// - Object, Array
// - Variables
// - References
// - Array Comprehensions
//
type Value interface {
// Equal returns true if this value equals the other value.
@@ -70,6 +71,11 @@ func (term *Term) Equal(other *Term) bool {
return term.Value.Equal(other.Value)
}
// Hash returns the hash code of the Term's value.
func (term *Term) Hash() int {
return term.Value.Hash()
}
// IsGround returns true if this terms' Value is ground.
func (term *Term) IsGround() bool {
return term.Value.IsGround()
@@ -97,6 +103,8 @@ func (term *Term) MarshalJSON() ([]byte, error) {
typ = "array"
case Object:
typ = "object"
case *ArrayComprehension:
typ = "array-comprehension"
}
d := map[string]interface{}{
"Type": typ,
@@ -124,6 +132,13 @@ func (term *Term) UnmarshalJSON(bs []byte) error {
return nil
}
// Vars returns a VarSet with variables contained in this term.
func (term *Term) Vars() VarSet {
vis := &varVisitor{vars: VarSet{}}
Walk(vis, term)
return vis.vars
}
// Null represents the null value defined by JSON.
type Null struct{}
@@ -407,6 +422,17 @@ func (ref Ref) Underlying() ([]interface{}, error) {
return r, nil
}
// OutputVars returns a VarSet containing variables that would be bound by evaluating
// this expression in isolation.
func (ref Ref) OutputVars() VarSet {
vis := &varVisitor{
vars: VarSet{},
skipRefHead: true,
}
Walk(vis, ref)
return vis.vars
}
// QueryIterator defines the interface for querying AST documents with references.
type QueryIterator func(map[Var]Value, Value) error
@@ -582,6 +608,48 @@ func (obj Object) queryRec(ref Ref, keys map[Var]Value, iter QueryIterator) erro
}
}
// ArrayComprehension represents an array comprehension as defined in the language.
type ArrayComprehension struct {
Term *Term
Body Body
}
// ArrayComprehensionTerm creates a new Term with an ArrayComprehension value.
func ArrayComprehensionTerm(term *Term, body Body) *Term {
return &Term{
Value: &ArrayComprehension{
Term: term,
Body: body,
},
}
}
// Equal returns true if this array comprehension is syntactically equal to another.
func (ac *ArrayComprehension) Equal(other Value) bool {
if ac == other {
return true
}
o, ok := other.(*ArrayComprehension)
if !ok {
return false
}
return o.Term.Equal(ac.Term) && o.Body.Equal(ac.Body)
}
// Hash returns the hash code of the Value.
func (ac *ArrayComprehension) Hash() int {
return ac.Term.Hash() + ac.Body.Hash()
}
// IsGround returns true if the Term and Body are ground.
func (ac *ArrayComprehension) IsGround() bool {
return ac.Term.IsGround() && ac.Body.IsGround()
}
func (ac *ArrayComprehension) String() string {
return "[" + ac.Term.String() + " | " + ac.Body.String() + "]"
}
func queryRec(v Value, ref Ref, tail Ref, keys map[Var]Value, iter QueryIterator, skipScalar bool) error {
if len(tail) == 0 {
if err := iter(keys, v); err != nil {
@@ -631,104 +699,148 @@ func termSliceIsGround(a []*Term) bool {
return true
}
func unmarshalError(v interface{}, e string) error {
return fmt.Errorf("ast: cannot unmarshal %T into Go value of type %v", v, e)
// NOTE(tsandall): The unmarshalling errors in these functions are not
// helpful for callers because they do not identify the source of the
// unmarshalling error. Because OPA doesn't accept JSON describing ASTs
// from callers, this is acceptable (for now). If that changes in the future,
// the error messages should be revisited. The current approach focuses
// on the happy path and treats all errors the same. If better error
// reporting is needed, the error paths will need to be fleshed out.
func unmarshalBody(b []interface{}) (Body, error) {
buf := Body{}
for _, e := range b {
if m, ok := e.(map[string]interface{}); ok {
expr := &Expr{}
if err := unmarshalExpr(expr, m); err == nil {
buf = append(buf, expr)
continue
}
}
goto unmarshal_error
}
return buf, nil
unmarshal_error:
return nil, fmt.Errorf("ast: unable to unmarshal body")
}
func unmarshalTermSlice(d map[string]interface{}) ([]*Term, error) {
s, ok := d["Value"].([]interface{})
if !ok {
return nil, unmarshalError(d["Value"], "[]interface{}")
func unmarshalExpr(expr *Expr, v map[string]interface{}) error {
if x, ok := v["Negated"]; ok {
if b, ok := x.(bool); ok {
expr.Negated = b
} else {
return fmt.Errorf("ast: unable to unmarshal Negated field with type: %T (expected true or false)", v["Negated"])
}
}
buf := []*Term{}
for _, i := range s {
m, ok := i.(map[string]interface{})
if !ok {
return nil, unmarshalError(i, "map[string]interface{}")
}
v, err := unmarshalValue(m)
switch ts := v["Terms"].(type) {
case map[string]interface{}:
t, err := unmarshalTerm(ts)
if err != nil {
return nil, err
return err
}
buf = append(buf, &Term{Value: v})
expr.Terms = t
case []interface{}:
terms, err := unmarshalTermSlice(ts)
if err != nil {
return err
}
expr.Terms = terms
default:
return fmt.Errorf(`ast: unable to unmarshal Terms field with type: %T (expected {"Value": ..., "Type": ...} or [{"Value": ..., "Type": ...}, ...])`, v["Terms"])
}
return nil
}
func unmarshalTerm(m map[string]interface{}) (*Term, error) {
v, err := unmarshalValue(m)
if err != nil {
return nil, err
}
return &Term{Value: v}, nil
}
func unmarshalTermSlice(s []interface{}) ([]*Term, error) {
buf := []*Term{}
for _, x := range s {
if m, ok := x.(map[string]interface{}); ok {
if t, err := unmarshalTerm(m); err == nil {
buf = append(buf, t)
continue
}
}
return nil, fmt.Errorf("ast: unable to unmarshal term")
}
return buf, nil
}
func unmarshalTermSliceValue(d map[string]interface{}) ([]*Term, error) {
if s, ok := d["Value"].([]interface{}); ok {
return unmarshalTermSlice(s)
}
return nil, fmt.Errorf(`ast: unable to unmarshal term (expected {"Value": [...], "Type": ...} where type is one of: array, reference)`)
}
func unmarshalValue(d map[string]interface{}) (Value, error) {
v := d["Value"]
switch d["Type"] {
case "null":
return Null{}, nil
case "boolean":
b, ok := d["Value"].(bool)
if !ok {
return nil, unmarshalError(d["Value"], "bool")
if b, ok := v.(bool); ok {
return Boolean(b), nil
}
return Boolean(b), nil
case "number":
f, ok := d["Value"].(float64)
if !ok {
return nil, unmarshalError(d["Value"], "float64")
if n, ok := v.(float64); ok {
return Number(n), nil
}
return Number(f), nil
case "string":
s, ok := d["Value"].(string)
if !ok {
return nil, unmarshalError(d["Value"], "string")
if s, ok := v.(string); ok {
return String(s), nil
}
return String(s), nil
case "ref":
s, err := unmarshalTermSlice(d)
if err != nil {
return nil, err
}
return Ref(s), nil
case "var":
s, ok := d["Value"].(string)
if !ok {
return nil, unmarshalError(d["Value"], "ast.Var")
if s, ok := v.(string); ok {
return Var(s), nil
}
case "ref":
if s, err := unmarshalTermSliceValue(d); err == nil {
return Ref(s), nil
}
return Var(s), nil
case "array":
s, err := unmarshalTermSlice(d)
if err != nil {
return nil, err
if s, err := unmarshalTermSliceValue(d); err == nil {
return Array(s), nil
}
return Array(s), nil
case "object":
buf := Object{}
s, ok := d["Value"].([]interface{})
if !ok {
return nil, unmarshalError(d["Value"], "[]interface{}")
if s, ok := v.([]interface{}); ok {
buf := Object{}
for _, x := range s {
if i, ok := x.([]interface{}); ok && len(i) == 2 {
p, err := unmarshalTermSlice(i)
if err == nil {
buf = append(buf, Item(p[0], p[1]))
continue
}
}
goto unmarshal_error
}
return buf, nil
}
for _, i := range s {
p, ok := i.([]interface{})
if !ok {
return nil, unmarshalError(i, "[]interface{}")
case "array-comprehension":
if m, ok := v.(map[string]interface{}); ok {
if t, ok := m["Term"].(map[string]interface{}); ok {
if term, err := unmarshalTerm(t); err == nil {
if b, ok := m["Body"].([]interface{}); ok {
if body, err := unmarshalBody(b); err == nil {
buf := &ArrayComprehension{
Term: term,
Body: body,
}
return buf, nil
}
}
}
}
if len(p) != 2 {
return nil, unmarshalError(p, "[2]interface{}")
}
km, ok := p[0].(map[string]interface{})
if !ok {
return nil, unmarshalError(p[0], "map[string]interface{}")
}
k, err := unmarshalValue(km)
if err != nil {
return nil, err
}
vm, ok := p[1].(map[string]interface{})
if !ok {
return nil, unmarshalError(p[1], "map[string]interface{}")
}
v, err := unmarshalValue(vm)
if err != nil {
return nil, err
}
buf = append(buf, [2]*Term{&Term{Value: k}, &Term{Value: v}})
}
return buf, nil
default:
return nil, fmt.Errorf("ast: cannot unmarshal Term with Type %v", d["Type"])
}
unmarshal_error:
return nil, fmt.Errorf("ast: unable to unmarshal term")
}
+57 -31
View File
@@ -109,38 +109,24 @@ func TestQuery(t *testing.T) {
func TestTermBadJSON(t *testing.T) {
assert := func(js string, exp error) {
term := Term{}
err := json.Unmarshal([]byte(js), &term)
if !reflect.DeepEqual(exp, err) {
t.Errorf("Expected %v but got: %v", exp, err)
}
input := `{
"Value": [[
{"Value": [{"Value": "a", "Type": "var"}, {"Value": "x", "Type": "string"}], "Type": "ref"},
{"Value": [{"Value": "x", "Type": "var"}], "Type": "array"}
], [
{"Value": 100, "Type": "array"},
{"Value": "foo", "Type": "string"}
]],
"Type": "object"
}`
term := Term{}
err := json.Unmarshal([]byte(input), &term)
expected := fmt.Errorf("ast: unable to unmarshal term")
if !reflect.DeepEqual(expected, err) {
t.Errorf("Expected %v but got: %v", expected, err)
}
castTests := []struct {
input string
val interface{}
expected string
}{
{`{"Value": null, "Type": "boolean"}`, nil, "bool"},
{`{"Value": false, "Type": "number"}`, false, "float64"},
{`{"Value": 100, "Type": "string"}`, 100.0, "string"},
{`{"Value": "hello", "Type": "number"}`, "hello", "float64"},
{`{"Value": 100, "Type": "var"}`, 100.0, "ast.Var"},
{`{"Value": "abc", "Type": "ref"}`, "abc", "[]interface{}"},
{`{"Value": ["abc"], "Type": "ref"}`, "abc", "map[string]interface{}"},
{`{"Value": "abc", "Type": "array"}`, "abc", "[]interface{}"},
{`{"Value": ["abc"], "Type": "array"}`, "abc", "map[string]interface{}"},
{`{"Value": "abc", "Type": "object"}`, "abc", "[]interface{}"},
{`{"Value": ["abc"], "Type": "object"}`, "abc", "[]interface{}"},
{`{"Value": [["abc"]], "Type": "object"}`, []interface{}{}, "[2]interface{}"},
{`{"Value": [["abc", "abc"]], "Type": "object"}`, "abc", "map[string]interface{}"},
{`{"Value": [[{"Value": "abc", "Type": "string"}, "abc"]], "Type": "object"}`, "abc", "map[string]interface{}"},
}
for _, tc := range castTests {
assert(tc.input, unmarshalError(tc.val, tc.expected))
}
}
func TestTermEqual(t *testing.T) {
@@ -155,6 +141,7 @@ func TestTermEqual(t *testing.T) {
assertTermEqual(t, ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3)), ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3)))
assertTermEqual(t, VarTerm("foo"), VarTerm("foo"))
assertTermEqual(t, RefTerm(VarTerm("foo"), VarTerm("i"), NumberTerm(2)), RefTerm(VarTerm("foo"), VarTerm("i"), NumberTerm(2)))
assertTermEqual(t, ArrayComprehensionTerm(VarTerm("x"), Body{&Expr{Terms: RefTerm(VarTerm("a"), VarTerm("i"))}}), ArrayComprehensionTerm(VarTerm("x"), Body{&Expr{Terms: RefTerm(VarTerm("a"), VarTerm("i"))}}))
assertTermNotEqual(t, NullTerm(), BooleanTerm(true))
assertTermNotEqual(t, BooleanTerm(true), BooleanTerm(false))
assertTermNotEqual(t, NumberTerm(5), NumberTerm(7))
@@ -167,6 +154,7 @@ func TestTermEqual(t *testing.T) {
assertTermNotEqual(t, ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3)), ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(4)))
assertTermNotEqual(t, VarTerm("foo"), VarTerm("bar"))
assertTermNotEqual(t, RefTerm(VarTerm("foo"), VarTerm("i"), NumberTerm(2)), RefTerm(VarTerm("foo"), StringTerm("i"), NumberTerm(2)))
assertTermNotEqual(t, ArrayComprehensionTerm(VarTerm("x"), Body{&Expr{Terms: RefTerm(VarTerm("a"), VarTerm("j"))}}), ArrayComprehensionTerm(VarTerm("x"), Body{&Expr{Terms: RefTerm(VarTerm("a"), VarTerm("i"))}}))
}
func TestHash(t *testing.T) {
@@ -178,7 +166,8 @@ func TestHash(t *testing.T) {
],
"e": {
100: a[i].b
}
},
"k": [ "foo" | true ]
}
`
@@ -192,6 +181,42 @@ func TestHash(t *testing.T) {
}
}
func TestTermIsGround(t *testing.T) {
tests := []struct {
note string
term string
expected bool
}{
{"null", "null", true},
{"string", `"foo"`, true},
{"number", "42.1", true},
{"boolean", "false", true},
{"var", "x", false},
{"ref ground", "a.b[0]", true},
{"ref non-ground", "a.b[i].x", false},
{"array ground", "[1,2,3]", true},
{"array non-ground", "[1,2,x]", false},
{"object ground", `{"a": 1}`, true},
{"object non-ground key", `{"x": 1, y: 2}`, false},
{"object non-ground value", `{"x": 1, "y": y}`, false},
{"array compr ground", `["a" | true]`, true},
{"array compr non-ground", `[x | x = a[i]]`, false},
}
for i, tc := range tests {
term := MustParseTerm(tc.term)
if term.IsGround() != tc.expected {
expected := "ground"
if !tc.expected {
expected = "non-ground"
}
t.Errorf("Expected term %v to be %s (test case %d: %v)", term, expected, i, tc.note)
}
}
}
func TestTermString(t *testing.T) {
assertToString(t, Null{}, "null")
assertToString(t, Boolean(true), "true")
@@ -209,6 +234,7 @@ func TestTermString(t *testing.T) {
assertToString(t, ArrayTerm().Value, "[]")
assertToString(t, ObjectTerm().Value, "{}")
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]")
assertToString(t, ArrayComprehensionTerm(ArrayTerm(VarTerm("x")), Body{&Expr{Terms: RefTerm(VarTerm("a"), VarTerm("i"))}}).Value, "[[x] | a[i]]")
}
func TestRefUnderlying(t *testing.T) {
+165
View File
@@ -0,0 +1,165 @@
// 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 ast
// Unify returns a set of variables that will be unified when the equality expression defined by
// terms a and b is evaluated. The unifier assumes that variables in the VarSet safe are already
// unified.
func Unify(safe VarSet, a *Term, b *Term) VarSet {
u := &unifier{
safe: safe,
unified: VarSet{},
unknown: map[Var]VarSet{},
}
u.unify(a, b)
return u.unified
}
type unifier struct {
safe VarSet
unified VarSet
unknown map[Var]VarSet
}
func (u *unifier) isSafe(x Var) bool {
return u.safe.Contains(x) || u.unified.Contains(x)
}
func (u *unifier) unify(a *Term, b *Term) {
switch a := a.Value.(type) {
case Var:
switch b := b.Value.(type) {
case Var:
if u.isSafe(b) {
u.markSafe(a)
} else if u.isSafe(a) {
u.markSafe(b)
} else {
u.markUnknown(a, b)
u.markUnknown(b, a)
}
case Array, Object:
u.unifyAll(a, b)
default:
u.markSafe(a)
}
case Ref:
switch b := b.Value.(type) {
case Var:
u.markSafe(b)
case Array, Object:
u.markAllSafe(b, a)
}
case *ArrayComprehension:
switch b := b.Value.(type) {
case Var:
u.markSafe(b)
case Array:
u.markAllSafe(b, a)
}
case Array:
switch b := b.Value.(type) {
case Var:
u.unifyAll(b, a)
case Ref, *ArrayComprehension:
u.markAllSafe(a, b)
case Array:
if len(a) == len(b) {
for i := range a {
u.unify(a[i], b[i])
}
}
}
case Object:
switch b := b.Value.(type) {
case Var:
u.unifyAll(b, a)
case Ref:
u.markAllSafe(a, b)
case Object:
if len(a) == len(b) {
for i := range a {
u.unify(a[i][1], b[i][1])
}
}
}
default:
switch b := b.Value.(type) {
case Var:
u.markSafe(b)
}
}
}
func (u *unifier) markAllSafe(x Value, y Value) {
vis := u.varVisitor()
Walk(vis, x)
for v := range vis.vars {
u.markSafe(v)
}
}
func (u *unifier) markSafe(x Var) {
u.unified.Add(x)
// Add dependencies of 'x' to safe set
vs := u.unknown[x]
delete(u.unknown, x)
for v := range vs {
u.markSafe(v)
}
// Add dependants of 'x' to safe set if they have no more
// dependencies.
for v, deps := range u.unknown {
if deps.Contains(x) {
delete(deps, x)
if len(deps) == 0 {
u.markSafe(v)
}
}
}
}
func (u *unifier) markUnknown(a, b Var) {
if _, ok := u.unknown[a]; !ok {
u.unknown[a] = NewVarSet()
}
u.unknown[a].Add(b)
}
func (u *unifier) unifyAll(a Var, b Value) {
if u.isSafe(a) {
u.markAllSafe(b, a)
} else {
vis := u.varVisitor()
Walk(vis, b)
unsafe := vis.vars.Diff(u.safe).Diff(u.unified)
if len(unsafe) == 0 {
u.markSafe(a)
} else {
for v := range unsafe {
u.markUnknown(a, v)
}
}
}
}
func (u *unifier) varVisitor() *varVisitor {
return &varVisitor{
skipRefHead: true,
skipObjectKeys: true,
skipClosures: true,
skipBuiltinNames: true,
vars: VarSet{},
}
}
+78
View File
@@ -0,0 +1,78 @@
// 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 ast
import "testing"
func TestUnify(t *testing.T) {
tests := []struct {
note string
expr string
safe string
expected string
}{
// collection cases
{"array/ref", "[1,2,x] = a[_]", "[a]", "[x]"},
{"array/ref (reversed)", "a[_] = [1,2,x]", "[a]", "[x]"},
{"array/var", "[1,2,x] = y", "[x]", "[y]"},
{"array/var (reversed)", "y = [1,2,x]", "[x]", "[y]"},
{"array/var-2", "[1,2,x] = y", "[y]", "[x]"},
{"array/var-2 (reversed)", "y = [1,2,x]", "[y]", "[x]"},
{"array/uneven", "[1,2,x] = [y,x]", "[]", "[]"},
{"array/uneven-2", "[1,2,x] = [y,x]", "[x]", "[]"},
{"object/ref", `{"x": x} = a[_]`, "[a]", "[x]"},
{"object/ref (reversed)", `a[_] = {"x": x}`, "[a]", "[x]"},
{"object/var", `{"x": 1, "y": x} = y`, "[x]", "[y]"},
{"object/var (reversed)", `y = {"x": 1, "y": x}`, "[x]", "[y]"},
{"object/var-2", `{"x": 1, "y": x} = y`, "[y]", "[x]"},
{"object/var-3", `{"x": 1, "y": x} = y`, "[]", "[]"},
{"object/uneven", `{"x": x, "y": 1} = {"x": y}`, "[]", "[]"},
{"object/uneven", `{"x": x, "y": 1} = {"x": y}`, "[x]", "[]"},
// transitive cases
{"trans/redundant", "[x, x] = [x, 0]", "[]", "[x]"},
{"trans/simple", "[x, 1] = [y, y]", "[]", "[y, x]"},
{"trans/array", "[x, y] = [y, [z, a]]", "[x]", "[a, y, z]"},
{"trans/object", `[x, y] = [y, {"a":a,"z":z}]`, "[x]", "[a, y, z]"},
{"trans/ref", "[x, y, [x, y, i]] = [1, a[i], z]", "[a, i]", "[x, y, z]"},
{"trans/lazy", "[x, z, 2] = [1, [y, x], y]", "[]", "[x, y, z]"},
{"trans/redundant-nested", "[x, z, z] = [1, [y, x], [2, 1]]", "[]", "[x, y, z]"},
{"trans/bidirectional", "[x, z, y] = [[z,y], [1,y], 2]", "[]", "[x, y, z]"},
{"trans/occurs", "[x, z, y] = [[y,z], [y, 1], [2, x]]", "[]", "[]"},
}
for i, tc := range tests {
expr := MustParseBody(tc.expr)[0]
safe := VarSet{}
for _, x := range MustParseTerm(tc.safe).Value.(Array) {
safe.Add(x.Value.(Var))
}
terms := expr.Terms.([]*Term)
if !terms[0].Value.Equal(Equality.Name) {
panic(terms)
}
a, b := terms[1], terms[2]
unified := Unify(safe, a, b)
result := VarSet{}
for k := range unified {
result.Add(k)
}
expected := VarSet{}
for _, x := range MustParseTerm(tc.expected).Value.(Array) {
expected.Add(x.Value.(Var))
}
missing := expected.Diff(result)
extra := result.Diff(expected)
if len(missing) != 0 || len(extra) != 0 {
t.Errorf("%s (%d): Missing vars: %v, extra vars: %v", tc.note, i, missing, extra)
}
}
}
+22
View File
@@ -41,6 +41,28 @@ func (s VarSet) Copy() VarSet {
return cpy
}
// Diff returns a VarSet containing variables in s that are not in vs.
func (s VarSet) Diff(vs VarSet) VarSet {
r := VarSet{}
for v := range s {
if !vs.Contains(v) {
r.Add(v)
}
}
return r
}
// Intersect returns a VarSet containing variables in s that are in vs.
func (s VarSet) Intersect(vs VarSet) VarSet {
r := VarSet{}
for v := range s {
if vs.Contains(v) {
r.Add(v)
}
}
return r
}
// Update merges the other VarSet into this VarSet.
func (s VarSet) Update(vs VarSet) {
for v := range vs {
+43
View File
@@ -72,5 +72,48 @@ func Walk(v Visitor, x interface{}) {
for _, t := range x {
Walk(w, t.Value)
}
case *ArrayComprehension:
Walk(w, x.Term)
Walk(w, x.Body)
}
}
// WalkClosures calls the function f on all closures under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkClosures(x interface{}, f func(interface{}) bool) {
vis := &GenericVisitor{func(x interface{}) bool {
switch x.(type) {
case *ArrayComprehension:
return f(x)
}
return false
}}
Walk(vis, x)
}
// WalkRefs calls the function f on all references under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkRefs(x interface{}, f func(Ref) bool) {
vis := &GenericVisitor{func(x interface{}) bool {
if r, ok := x.(Ref); ok {
return f(r)
}
return false
}}
Walk(vis, x)
}
// GenericVisitor implements the Visitor interface to provide
// a utility to walk over AST nodes using a closure. If the closure
// returns true, the visitor will not walk over AST nodes under x.
type GenericVisitor struct {
f func(x interface{}) bool
}
// Visit calls the function f on the GenericVisitor.
func (vis *GenericVisitor) Visit(x interface{}) Visitor {
if vis.f(x) {
return nil
}
return vis
}
+22 -3
View File
@@ -20,7 +20,10 @@ func TestVisitor(t *testing.T) {
rule := MustParseModule(`
package a.b
import x.y as z
t[x] = y :- p[x] = {"foo": [y,2,{"bar": 3}]}, not q[x]
t[x] = y :-
p[x] = {"foo": [y,2,{"bar": 3}]},
not q[x],
y = [ [x,z] | x = "x", z = "z" ]
`)
vis := &testVis{}
Walk(vis, rule)
@@ -59,9 +62,25 @@ func TestVisitor(t *testing.T) {
ref2
q
x
expr3
=
y
compr
array
x
z
body
expr4
=
x
"x"
expr5
=
z
"z"
*/
if len(vis.elems) != 33 {
t.Errorf("Expected exactly 33 elements in AST but got %d: %v", len(vis.elems), vis.elems)
if len(vis.elems) != 49 {
t.Errorf("Expected exactly 49 elements in AST but got %d: %v", len(vis.elems), vis.elems)
}
}
+67 -1
View File
@@ -470,6 +470,71 @@ The result:
+-------+
```
## <a name="comprehensions"></a> Comprehensions
Comprehensions provide a concise way of building [Composite Values](#composite-values) from sub-queries.
Like [Rules](#rules), comprehensions consist of a head and a body. The body of a comprehension can be understood in exactly the same way as the body of a rule, that is, one or more expressions that must all be true in order for the overall body to be true. When the body evaluates to true, the head of the comprehension is evaluated to produce an element in the result.
The body of a comprehension is able to refer to variables defined in the outer body. For example:
```
> region = "west", names = [name | sites[i].region = region, sites[i].name = name]
+-----------------+--------+
| NAMES | REGION |
+-----------------+--------+
| ["smoke","dev"] | "west" |
+-----------------+--------+
```
In the above query, the second expression contains an [Array Comprehension](#array-comprehension) that refers to the "region" variable. The region variable will be bound in the outer body.
> When a comprehension refers to a variable in an outer body, OPA will reorder expressions in the outer body so that variables referred to in the comprehension are bound by the time the comprehension is evaluated.
Comprehensions are similar to the same constructs found in other languages like Python. For example, we could write the above comprehension in Python as follows:
```python
# Python equivalent of Rego comprehension shown above.
names = [site.name for site in sites if site.region = "west"]
```
Comprehensions are often used to group elements by some key. A common use case for comprehensions is to assist in computing aggregate values (e.g., the number of containers running on a host).
### <a name="array-comprehension"></a> Array Comprehensions
Array Comprehensions build array values out of sub-queries. Array Comprehensions have the form:
```
[ <term> | <body> ]
```
For example, the following rule defines an object where the keys are application names and the values are hostnames of servers where the application is deployed. The hostnames of servers are represented as an array.
```rego
app_to_hostnames[app_name] = hostnames :-
apps[_] = app,
app_name = app.name,
hostnames = [hostname | name = app.servers[_],
sites[_].servers[_] = s,
s.name = name,
hostname = s.hostname]
```
The result:
```
> app_to_hostnames[app] = hostnames
+-----------+-----------------------------------------------------+
| APP | HOSTNAMES |
+-----------+-----------------------------------------------------+
| "web" | ["hydrogen","helium","berylium","boron","nitrogen"] |
| "mysql" | ["lithium","carbon"] |
| "mongodb" | ["oxygen"] |
+-----------+-----------------------------------------------------+
```
In the future, Rego will support Set and Object comprehensions.
## <a name="rules"></a> Rules
Rules define the content of [Virtual Documents](/docs/arch.html#data-model) in
@@ -864,7 +929,8 @@ literal = expr | "not" expr
expr = term | expr-builtin | expr-infix
expr-builtin = var "(" [ term { , term } ] ")"
expr-infix = term bool-operator term
term = ref | var | scalar | array | object
term = ref | var | scalar | array | object | array-compr
array-compr = "[" term "|" rule-body "]"
bool-operator = "=" | "!=" | "<" | ">" | ">=" | "<="
ref = var { ref-arg }
ref-arg = ref-arg-dot | ref-arg-brack
+68
View File
@@ -0,0 +1,68 @@
// 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 topdown
import (
"fmt"
"github.com/open-policy-agent/opa/ast"
"github.com/pkg/errors"
)
func evalCount(ctx *Context, expr *ast.Expr, iter Iterator) error {
ops := expr.Terms.([]*ast.Term)
src, dst := ops[1].Value, ops[2].Value
s, err := ValueToInterface(src, ctx)
if err != nil {
return errors.Wrapf(err, "count")
}
var count ast.Number
switch s := s.(type) {
case []interface{}:
count = ast.Number(len(s))
case map[string]interface{}:
count = ast.Number(len(s))
default:
return fmt.Errorf("count: source must be a collection: %v", src)
}
switch dst := dst.(type) {
case ast.Var:
ctx = ctx.BindVar(dst, count)
return iter(ctx)
default:
if dst.Equal(count) {
return iter(ctx)
}
return nil
}
}
func evalSum(ctx *Context, expr *ast.Expr, iter Iterator) error {
ops := expr.Terms.([]*ast.Term)
src, dst := ops[1].Value, ops[2].Value
s, err := ValueToSlice(src, ctx)
if err != nil {
return errors.Wrapf(err, "sum")
}
sum := ast.Number(0)
for _, x := range s {
sum += ast.Number(x.(float64))
}
switch dst := dst.(type) {
case ast.Var:
ctx = ctx.BindVar(dst, sum)
return iter(ctx)
default:
if dst.Equal(sum) {
return iter(ctx)
}
return nil
}
}
+92
View File
@@ -0,0 +1,92 @@
// 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 topdown
import (
"fmt"
"math"
"github.com/open-policy-agent/opa/ast"
"github.com/pkg/errors"
)
type arithmeticFunc func(a, b float64) (ast.Number, error)
func arithPlus(a, b float64) (ast.Number, error) {
return ast.Number(a + b), nil
}
func arithMinus(a, b float64) (ast.Number, error) {
return ast.Number(a - b), nil
}
func arithMultiply(a, b float64) (ast.Number, error) {
return ast.Number(a * b), nil
}
func arithDivide(a, b float64) (ast.Number, error) {
if b == 0 {
return 0, fmt.Errorf("divide: by zero")
}
return ast.Number(a / b), nil
}
func arithRound(a float64) (ast.Number, error) {
return ast.Number(math.Floor(a + 0.5)), nil
}
func evalRound(ctx *Context, expr *ast.Expr, iter Iterator) error {
ops := expr.Terms.([]*ast.Term)
a, err := ValueToFloat64(ops[1].Value, ctx)
if err != nil {
return errors.Wrapf(err, "round")
}
r := ast.Number(math.Floor(a + 0.5))
b := ops[2].Value
switch b := b.(type) {
case ast.Var:
ctx = ctx.BindVar(b, r)
return iter(ctx)
default:
if b.Equal(r) {
return iter(ctx)
}
return nil
}
}
func evalArithmetic(f arithmeticFunc) BuiltinFunc {
return func(ctx *Context, expr *ast.Expr, iter Iterator) error {
ops := expr.Terms.([]*ast.Term)
a, err := ValueToFloat64(ops[1].Value, ctx)
if err != nil {
return errors.Wrapf(err, "arithemtic")
}
b, err := ValueToFloat64(ops[2].Value, ctx)
if err != nil {
return errors.Wrapf(err, "arithemtic")
}
c, err := f(a, b)
if err != nil {
return err
}
cv := ops[3].Value
switch cv := cv.(type) {
case ast.Var:
ctx = ctx.BindVar(cv, c)
return iter(ctx)
default:
if cv.Equal(c) {
return iter(ctx)
}
return nil
}
}
}
+8
View File
@@ -31,6 +31,14 @@ var defaultBuiltinFuncs = map[ast.Var]BuiltinFunc{
ast.LessThan.Name: evalIneq(compareLessThan),
ast.LessThanEq.Name: evalIneq(compareLessThanEq),
ast.NotEqual.Name: evalIneq(compareNotEq),
ast.Plus.Name: evalArithmetic(arithPlus),
ast.Minus.Name: evalArithmetic(arithMinus),
ast.Multiply.Name: evalArithmetic(arithMultiply),
ast.Divide.Name: evalArithmetic(arithDivide),
ast.Round.Name: evalRound,
ast.Count.Name: evalCount,
ast.Sum.Name: evalSum,
ast.ToNumber.Name: evalToNumber,
}
func init() {
+55
View File
@@ -0,0 +1,55 @@
// 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 topdown
import (
"fmt"
"strconv"
"github.com/open-policy-agent/opa/ast"
"github.com/pkg/errors"
)
func evalToNumber(ctx *Context, expr *ast.Expr, iter Iterator) error {
ops := expr.Terms.([]*ast.Term)
a, b := ops[1].Value, ops[2].Value
x, err := ValueToInterface(a, ctx)
if err != nil {
return fmt.Errorf("to_number")
}
var n ast.Number
switch x := x.(type) {
case string:
f, err := strconv.ParseFloat(string(x), 64)
if err != nil {
return errors.Wrapf(err, "to_number")
}
n = ast.Number(f)
case float64:
n = ast.Number(x)
case bool:
if x {
n = ast.Number(1)
} else {
n = ast.Number(0)
}
default:
return fmt.Errorf("to_number: source must be a string, boolean, or number: %T", a)
}
switch b := b.(type) {
case ast.Var:
ctx = ctx.BindVar(b, n)
return iter(ctx)
default:
if n.Equal(b) {
return iter(ctx)
}
return nil
}
}
+79 -66
View File
@@ -50,11 +50,11 @@ func (ctx *Context) Binding(k ast.Value) ast.Value {
return nil
}
// BindRef returns a new Context with bindings that map the reference to the value.
func (ctx *Context) BindRef(ref ast.Ref, value ast.Value) *Context {
// BindValue returns a new Context with bindings that map the key to the value.
func (ctx *Context) BindValue(key ast.Value, value ast.Value) *Context {
cpy := *ctx
cpy.Locals = ctx.Locals.Copy()
cpy.Locals.Put(ref, value)
cpy.Locals.Put(key, value)
return &cpy
}
@@ -78,15 +78,6 @@ func (ctx *Context) BindVar(variable ast.Var, value ast.Value) *Context {
if variable.Equal(value) {
return ctx
}
occurs := walkValue(value, func(other ast.Value) bool {
if variable.Equal(other) {
return true
}
return false
})
if occurs {
return nil
}
cpy := *ctx
cpy.Locals = storage.NewBindings()
@@ -106,10 +97,10 @@ func (ctx *Context) BindVar(variable ast.Var, value ast.Value) *Context {
return &cpy
}
// Child returns a new context to evaluate a rule that was referenced by this context.
func (ctx *Context) Child(rule *ast.Rule, locals *storage.Bindings) *Context {
// Child returns a new context to evaluate a query that was referenced by this context.
func (ctx *Context) Child(query ast.Body, locals *storage.Bindings) *Context {
cpy := *ctx
cpy.Query = rule.Body
cpy.Query = query
cpy.Locals = locals
cpy.Previous = ctx
cpy.Index = 0
@@ -389,23 +380,6 @@ func dereferenceVar(v ast.Var, ctx *Context) (interface{}, error) {
func evalContext(ctx *Context, iter Iterator) error {
if ctx.Index >= len(ctx.Query) {
// Check if the bindings contain values that are non-ground. E.g.,
// suppose the query's final expression is "x = y" and "x" and "y"
// do not appear elsewhere in the query. In this case, "x" and "y"
// will be bound to each other; they will not be ground and so
// the proof should not be considered successful.
isNonGround := ctx.Locals.Iter(func(k, v ast.Value) bool {
if !v.IsGround() {
return true
}
return false
})
if isNonGround {
return nil
}
ctx.traceFinish()
return iter(ctx)
}
@@ -465,15 +439,13 @@ func evalExpr(ctx *Context, iter Iterator) error {
return iter(ctx)
})
case *ast.Term:
switch tv := tt.Value.(type) {
case ast.Boolean:
if tv.Equal(ast.Boolean(true)) {
v := tt.Value
if !v.Equal(ast.Boolean(false)) {
if v.IsGround() {
return iter(ctx)
}
return nil
default:
return fmt.Errorf("illegal implicit cast: %v", tv)
}
return nil
default:
panic(fmt.Sprintf("illegal argument: %v", tt))
}
@@ -628,7 +600,7 @@ func evalRefRuleCompleteDoc(ctx *Context, ref ast.Ref, suffix ast.Ref, rules []*
for _, rule := range rules {
bindings := storage.NewBindings()
child := ctx.Child(rule, bindings)
child := ctx.Child(rule.Body, bindings)
isTrue := false
err := Eval(child, func(child *Context) error {
@@ -673,7 +645,7 @@ func evalRefRulePartialObjectDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *
// NOTE: if at some point multiple variables are supported here, it may be
// cleaner to generalize this (instead of having two separate branches).
if !key.IsGround() {
child := ctx.Child(rule, storage.NewBindings())
child := ctx.Child(rule.Body, storage.NewBindings())
return Eval(child, func(child *Context) error {
key := child.Binding(rule.Key.Value)
if key == nil {
@@ -690,7 +662,7 @@ func evalRefRulePartialObjectDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *
bindings := storage.NewBindings()
bindings.Put(rule.Key.Value, key)
child := ctx.Child(rule, bindings)
child := ctx.Child(rule.Body, bindings)
return Eval(child, func(child *Context) error {
value := child.Binding(rule.Value.Value)
@@ -713,7 +685,7 @@ func evalRefRulePartialObjectDocFull(ctx *Context, ref ast.Ref, rules []*ast.Rul
for _, rule := range rules {
bindings := storage.NewBindings()
child := ctx.Child(rule, bindings)
child := ctx.Child(rule.Body, bindings)
err := Eval(child, func(child *Context) error {
key := child.Binding(rule.Key.Value)
@@ -731,7 +703,7 @@ func evalRefRulePartialObjectDocFull(ctx *Context, ref ast.Ref, rules []*ast.Rul
}
}
ctx = ctx.BindRef(ref, result)
ctx = ctx.BindValue(ref, result)
return iter(ctx)
}
@@ -751,7 +723,7 @@ func evalRefRulePartialSetDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *ast
key := plugValue(suffix[0].Value, ctx)
if !key.IsGround() {
child := ctx.Child(rule, storage.NewBindings())
child := ctx.Child(rule.Body, storage.NewBindings())
return Eval(child, func(child *Context) error {
value := child.Binding(rule.Key.Value)
if value == nil {
@@ -763,18 +735,18 @@ func evalRefRulePartialSetDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *ast
// "p = true :- q[x]", we say that "p" should be defined if "q"
// is defined for some value "x".
ctx = ctx.BindVar(key.(ast.Var), value)
ctx = ctx.BindRef(ref[:len(path)+1], ast.Boolean(true))
ctx = ctx.BindValue(ref[:len(path)+1], ast.Boolean(true))
return iter(ctx)
})
}
bindings := storage.NewBindings()
bindings.Put(rule.Key.Value, key)
child := ctx.Child(rule, bindings)
child := ctx.Child(rule.Body, bindings)
return Eval(child, func(child *Context) error {
// See comment above for explanation of why the reference is bound to true.
ctx = ctx.BindRef(ref[:len(path)+1], ast.Boolean(true))
ctx = ctx.BindValue(ref[:len(path)+1], ast.Boolean(true))
return iter(ctx)
})
@@ -797,7 +769,7 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
binding = append(binding, result...)
binding = append(binding, suffix...)
return evalRefRec(ctx, result, suffix, func(ctx *Context) error {
ctx = ctx.BindRef(ref, plugValue(binding, ctx))
ctx = ctx.BindValue(ref, plugValue(binding, ctx))
return iter(ctx)
})
@@ -808,14 +780,14 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
pluggedSuffix = append(pluggedSuffix, plugTerm(t, ctx))
}
return result.Query(pluggedSuffix, func(keys map[ast.Var]ast.Value, value ast.Value) error {
ctx = ctx.BindRef(ref, value)
ctx = ctx.BindValue(ref, value)
for k, v := range keys {
ctx = ctx.BindVar(k, v)
}
return iter(ctx)
})
}
ctx = ctx.BindRef(ref, result)
ctx = ctx.BindValue(ref, result)
return iter(ctx)
case ast.Object:
@@ -825,14 +797,14 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
pluggedSuffix = append(pluggedSuffix, plugTerm(t, ctx))
}
return result.Query(pluggedSuffix, func(keys map[ast.Var]ast.Value, value ast.Value) error {
ctx = ctx.BindRef(ref, value)
ctx = ctx.BindValue(ref, value)
for k, v := range keys {
ctx = ctx.BindVar(k, v)
}
return iter(ctx)
})
}
ctx = ctx.BindRef(ref, result)
ctx = ctx.BindValue(ref, result)
return iter(ctx)
default:
@@ -840,18 +812,12 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
// This is not defined because it attempts to dereference a scalar.
return nil
}
ctx = ctx.BindRef(ref, result)
ctx = ctx.BindValue(ref, result)
return iter(ctx)
}
}
// evalTerms is used to get bindings for variables in individual terms.
//
// Before an expression is evaluated, this function is called to find bindings
// for variables used in references inside the expression. Finding bindings for
// variables used in references involves iterating collections in storage or
// evaluating rules identified by the references. In either case, this function
// will invoke the iterator with each set of bindings that should be evaluated.
// TODO(tsandall):
func evalTerms(ctx *Context, iter Iterator) error {
expr := ctx.Current()
@@ -902,6 +868,25 @@ func evalTerms(ctx *Context, iter Iterator) error {
return evalTermsRec(ctx, iter, ts)
}
func evalTermsComprehension(ctx *Context, comp ast.Value, iter Iterator) error {
switch comp := comp.(type) {
case *ast.ArrayComprehension:
r := ast.Array{}
c := ctx.Child(comp.Body, ctx.Locals)
err := Eval(c, func(c *Context) error {
r = append(r, plugTerm(comp.Term, c))
return nil
})
if err != nil {
return err
}
ctx = ctx.BindValue(comp, r)
return iter(ctx)
default:
panic(fmt.Sprintf("illegal argument: %v %v", ctx, comp))
}
}
func evalTermsIndexed(ctx *Context, iter Iterator, indexed ast.Ref, nonIndexed *ast.Term) error {
iterateIndex := func(ctx *Context) error {
@@ -953,6 +938,10 @@ func evalTermsRec(ctx *Context, iter Iterator, ts []*ast.Term) error {
return evalTermsRecObject(ctx, head, 0, func(ctx *Context) error {
return evalTermsRec(ctx, iter, tail)
})
case *ast.ArrayComprehension:
return evalTermsComprehension(ctx, head, func(ctx *Context) error {
return evalTermsRec(ctx, iter, tail)
})
default:
return evalTermsRec(ctx, iter, tail)
}
@@ -975,6 +964,10 @@ func evalTermsRecArray(ctx *Context, arr ast.Array, idx int, iter Iterator) erro
return evalTermsRecObject(ctx, v, 0, func(ctx *Context) error {
return evalTermsRecArray(ctx, arr, idx+1, iter)
})
case *ast.ArrayComprehension:
return evalTermsComprehension(ctx, v, func(ctx *Context) error {
return evalTermsRecArray(ctx, arr, idx+1, iter)
})
default:
return evalTermsRecArray(ctx, arr, idx+1, iter)
}
@@ -1000,6 +993,10 @@ func evalTermsRecObject(ctx *Context, obj ast.Object, idx int, iter Iterator) er
return evalTermsRecObject(ctx, v, 0, func(ctx *Context) error {
return evalTermsRecObject(ctx, obj, idx+1, iter)
})
case *ast.ArrayComprehension:
return evalTermsComprehension(ctx, v, func(ctx *Context) error {
return evalTermsRecObject(ctx, obj, idx+1, iter)
})
default:
return evalTermsRecObject(ctx, obj, idx+1, iter)
}
@@ -1018,6 +1015,10 @@ func evalTermsRecObject(ctx *Context, obj ast.Object, idx int, iter Iterator) er
return evalTermsRecObject(ctx, v, 0, func(ctx *Context) error {
return evalTermsRecObject(ctx, obj, idx+1, iter)
})
case *ast.ArrayComprehension:
return evalTermsComprehension(ctx, v, func(ctx *Context) error {
return evalTermsRecObject(ctx, obj, idx+1, iter)
})
default:
return evalTermsRecObject(ctx, obj, idx+1, iter)
}
@@ -1165,6 +1166,11 @@ func plugTerm(term *ast.Term, ctx *Context) *ast.Term {
plugged.Value = plugValue(v, ctx)
return &plugged
case *ast.ArrayComprehension:
plugged := *term
plugged.Value = plugValue(v, ctx)
return &plugged
default:
if !term.IsGround() {
panic("unreachable")
@@ -1177,15 +1183,22 @@ func plugValue(v ast.Value, ctx *Context) ast.Value {
switch v := v.(type) {
case ast.Var:
binding := ctx.Binding(v)
if binding == nil {
b := ctx.Binding(v)
if b == nil {
return v
}
return binding.(ast.Value)
return b
case *ast.ArrayComprehension:
b := ctx.Binding(v)
if b == nil {
return v
}
return b
case ast.Ref:
if binding := ctx.Binding(v); binding != nil {
return binding.(ast.Value)
if b := ctx.Binding(v); b != nil {
return b
}
if v.IsGround() {
return v
@@ -1215,7 +1228,7 @@ func plugValue(v ast.Value, ctx *Context) ast.Value {
default:
if !v.IsGround() {
panic("unreachable")
panic(fmt.Sprintf("illegal value: %v %v", ctx, v))
}
return v
}
+111 -6
View File
@@ -234,7 +234,6 @@ func TestTopDownCompleteDoc(t *testing.T) {
{`object/nested composites: {"a": [1], "b": [2], "c": [3]}`,
`p = {"a": [1], "b": [2], "c": [3]} :- true`,
`{"a": [1], "b": [2], "c": [3]}`},
{"var/var", "p = true :- x = y", ""},
}
data := loadSmallTestData()
@@ -258,7 +257,6 @@ func TestTopDownPartialSetDoc(t *testing.T) {
{"nested composites", "p[x] :- f[i] = x", `[{"xs": [1.0], "ys": [2.0]}, {"xs": [2.0], "ys": [3.0]}]`},
{"deep ref/heterogeneous", "p[x] :- c[i][j][k] = x", `[null, 3.14159, true, false, true, false, "foo"]`},
{"composite var value", "p[x] :- x = [i, a[i]]", "[[0,1],[1,2],[2,3],[3,4]]"},
{"var/var", "p[x] :- x = y", "[]"},
}
data := loadSmallTestData()
@@ -278,8 +276,38 @@ func TestTopDownPartialObjectDoc(t *testing.T) {
{"composites", "p[k] = v :- d[k] = v", `{"e": ["bar", "baz"]}`},
{"non-string key", "p[k] = v :- a[k] = v", fmt.Errorf("illegal object key type float64: 0")},
{"body/join var", "p[k] = v :- a[i] = v, g[k][i] = v", `{"a": 1, "b": 2, "c": 4}`},
{"var/var key", "p[k] = v :- v = 1, k = x", "{}"},
{"var/var val", `p[k] = v :- k = "x", v = x`, "{}"},
}
data := loadSmallTestData()
for i, tc := range tests {
runTopDownTestCase(t, data, i, tc.note, []string{tc.rule}, tc.expected)
}
}
func TestTopDownEvalTermExpr(t *testing.T) {
tests := []struct {
note string
rule string
expected string
}{
{"true", "p :- true", "true"},
{"false", "p :- false", ""},
{"number non-zero", "p :- -3.14", "true"},
{"number zero", "p :- null", "true"},
{"null", "p :- null", "true"},
{"string non-empty", `p :- "abc"`, "true"},
{"string empty", `p :- ""`, "true"},
{"array non-empty", "p :- [1,2,3]", "true"},
{"array empty", "p :- []", "true"},
{"object non-empty", `p :- {"a": 1}`, "true"},
{"object empty", `p :- {}`, "true"},
{"ref", "p :- a[i]", "true"},
{"ref undefined", "p :- data.deadbeef[i]", ""},
{"array comprehension", "p :- [x | x = 1]", "true"},
{"array comprehension empty", "p :- [x | x = 1, x = 2]", "true"},
{"arbitrary position", "p :- a[i] = x, x, i", "true"},
}
data := loadSmallTestData()
@@ -310,8 +338,6 @@ func TestTopDownEqExpr(t *testing.T) {
{"undefined: array deep var 2", "p = true :- [[1,x],[3,4]] = [[1,2],[x,4]]", ""},
{"undefined: array uneven", `p = true :- [true, false, "foo", "deadbeef"] = c[i][j]`, ""},
{"undefined: object uneven", `p = true :- {"a": 1, "b": 2} = {"a": 1}`, ""},
{"undefined: occurs 1", "p = true :- [y,x] = [[x],y]", ""},
{"undefined: occurs 2", "p = true :- [y,x] = [{\"a\": x}, y]", ""},
// ground terms
{"ground: bool", `p = true :- true = true`, "true"},
@@ -560,6 +586,85 @@ func TestTopDownNegation(t *testing.T) {
}
}
func TestTopDownComprehensions(t *testing.T) {
tests := []struct {
note string
rules []string
expected interface{}
}{
{"simple", []string{"p[i] :- xs = [x | x = a[_]], xs[i] > 1"}, "[1,2,3]"},
{"nested", []string{"p[i] :- ys = [y | y = x[_], x = [z | z = a[_]]], ys[i] > 1"}, "[1,2,3]"},
{"embedded array", []string{"p[i] :- xs = [[x | x = a[_]]], xs[0][i] > 1"}, "[1,2,3]"},
{"embedded object", []string{`p[i] :- xs = {"a": [x | x = a[_]]}, xs["a"][i] > 1`}, "[1,2,3]"},
{"closure", []string{"p[x] :- y = 1, x = [y | y = 1]"}, "[[1]]"},
}
data := loadSmallTestData()
for i, tc := range tests {
runTopDownTestCase(t, data, i, tc.note, tc.rules, tc.expected)
}
}
func TestTopDownAggregates(t *testing.T) {
tests := []struct {
note string
rules []string
expected interface{}
}{
{"count", []string{"p[x] :- count(a, x)"}, "[4]"},
{"count virtual", []string{"p[x] :- count([y | q[y]], x)", "q[x] :- x = a[_]"}, "[4]"},
{"count keys", []string{"p[x] :- count(b, x)"}, "[2]"},
{"count keys virtual", []string{"p[x] :- count([k | q[k] = _], x)", "q[k] = v :- b[k] = v"}, "[2]"},
{"sum", []string{"p[x] :- sum([1,2,3,4], x)"}, "[10]"},
{"sum virtual", []string{"p[x] :- sum([y | q[y]], x)", "q[x] :- a[_] = x"}, "[10]"},
}
data := loadSmallTestData()
for i, tc := range tests {
runTopDownTestCase(t, data, i, tc.note, tc.rules, tc.expected)
}
}
func TestTopDownArithmetic(t *testing.T) {
tests := []struct {
note string
rules []string
expected interface{}
}{
{"plus", []string{"p[y] :- a[i] = x, plus(i, x, y)"}, "[1,3,5,7]"},
{"minus", []string{"p[y] :- a[i] = x, minus(i, x, y)"}, "[-1,-1,-1,-1]"},
{"multiply", []string{"p[y] :- a[i] = x, mul(i, x, y)"}, "[0,2,6,12]"},
{"divide+round", []string{"p[z] :- a[i] = x, div(i, x, y), round(y, z)"}, "[0,1,1,1]"},
{"divide+error", []string{"p[y] :- a[i] = x, div(x, i, y)"}, fmt.Errorf("divide: by zero")},
}
data := loadSmallTestData()
for i, tc := range tests {
runTopDownTestCase(t, data, i, tc.note, tc.rules, tc.expected)
}
}
func TestTopDownCasts(t *testing.T) {
tests := []struct {
note string
rules []string
expected interface{}
}{
{"to_number", []string{`p[x] :- to_number("-42.0", y), to_number(false, z), x = [y, z]`}, "[[-42.0, 0]]"},
}
data := loadSmallTestData()
for i, tc := range tests {
runTopDownTestCase(t, data, i, tc.note, tc.rules, tc.expected)
}
}
func TestTopDownEmbeddedVirtualDoc(t *testing.T) {
mods := compileModules([]string{