mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
+92
-24
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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{},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user