Files
releases/ast/env.go
T
Torin Sandall 3672a3f892 Improve call result handling
Previously, if callers omitted output terms from call expressions, the
result would be ignored. This was fine for most calls which would only
return true if they were defined, however, for functions could return
false this became confusing because an expression like "f(1)" where f(1)
= false would be succeed and yield a result.

With these changes, built-in functions that used to only return true
always return true or false and the eval engine takes care to check if
the result is false when the the caller omits the output term.

This provides consistent behaviour across cases like...

f(1)            => undefined (previously {})
f(1,x)          => {x:false} (previously {x:false})
neq(1,1,x)      => {x:false} (previously undefined)
neq(1,1,false)  => {}        (previously undefined)
neq(1,1,true)   => undefined (previously undefined)

In the next set of changes, the Rego package will be updated to capture
values for expressions like the first one above so that function calls
behave like refs (i.e., their values are returned).
2018-02-17 08:21:04 -08:00

324 lines
6.6 KiB
Go

// Copyright 2017 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 (
"github.com/open-policy-agent/opa/types"
"github.com/open-policy-agent/opa/util"
)
// TypeEnv contains type info for static analysis such as type checking.
type TypeEnv struct {
tree *typeTreeNode
next *TypeEnv
}
// NewTypeEnv returns an empty TypeEnv.
func NewTypeEnv() *TypeEnv {
return &TypeEnv{
tree: newTypeTree(),
}
}
// Get returns the type of x.
func (env *TypeEnv) Get(x interface{}) types.Type {
if term, ok := x.(*Term); ok {
x = term.Value
}
switch x := x.(type) {
// Scalars.
case Null:
return types.NewNull()
case Boolean:
return types.NewBoolean()
case Number:
return types.NewNumber()
case String:
return types.NewString()
// Composites.
case Array:
static := make([]types.Type, len(x))
for i := range static {
tpe := env.Get(x[i].Value)
static[i] = tpe
}
var dynamic types.Type
if len(static) == 0 {
dynamic = types.A
}
return types.NewArray(static, dynamic)
case Object:
static := []*types.StaticProperty{}
var dynamic *types.DynamicProperty
x.Foreach(func(k, v *Term) {
if IsConstant(k.Value) {
kjson, err := JSON(k.Value)
if err != nil {
panic("unreachable")
}
tpe := env.Get(v)
static = append(static, types.NewStaticProperty(kjson, tpe))
} else {
typeK := env.Get(k.Value)
typeV := env.Get(v.Value)
dynamic = types.NewDynamicProperty(typeK, typeV)
}
})
if len(static) == 0 && dynamic == nil {
dynamic = types.NewDynamicProperty(types.A, types.A)
}
return types.NewObject(static, dynamic)
case Set:
var tpe types.Type
x.Foreach(func(elem *Term) {
other := env.Get(elem.Value)
tpe = types.Or(tpe, other)
})
if tpe == nil {
tpe = types.A
}
return types.NewSet(tpe)
// Comprehensions.
case *ArrayComprehension:
checker := newTypeChecker()
cpy, errs := checker.CheckBody(env, x.Body)
if len(errs) == 0 {
return types.NewArray(nil, cpy.Get(x.Term))
}
return nil
case *ObjectComprehension:
checker := newTypeChecker()
cpy, errs := checker.CheckBody(env, x.Body)
if len(errs) == 0 {
return types.NewObject(nil, types.NewDynamicProperty(cpy.Get(x.Key), cpy.Get(x.Value)))
}
return nil
case *SetComprehension:
checker := newTypeChecker()
cpy, errs := checker.CheckBody(env, x.Body)
if len(errs) == 0 {
return types.NewSet(cpy.Get(x.Term))
}
return nil
// Refs.
case Ref:
return env.getRef(x)
// Vars.
case Var:
if node := env.tree.Child(x); node != nil {
return node.Value()
}
if env.next != nil {
return env.next.Get(x)
}
return nil
default:
panic("unreachable")
}
}
func (env *TypeEnv) getRef(ref Ref) types.Type {
node := env.tree.Child(ref[0].Value)
if node == nil {
return env.getRefFallback(ref)
}
return env.getRefRec(node, ref, ref[1:])
}
func (env *TypeEnv) getRefFallback(ref Ref) types.Type {
if env.next != nil {
return env.next.Get(ref)
}
if RootDocumentNames.Contains(ref[0]) {
return types.A
}
return nil
}
func (env *TypeEnv) getRefRec(node *typeTreeNode, ref, tail Ref) types.Type {
if len(tail) == 0 {
return env.getRefRecExtent(node)
}
if node.Leaf() {
return selectRef(node.Value(), tail)
}
if !IsConstant(tail[0].Value) {
return selectRef(env.getRefRecExtent(node), tail)
}
child := node.Child(tail[0].Value)
if child == nil {
return env.getRefFallback(ref)
}
return env.getRefRec(child, ref, tail[1:])
}
func (env *TypeEnv) getRefRecExtent(node *typeTreeNode) types.Type {
if node.Leaf() {
return node.Value()
}
children := []*types.StaticProperty{}
node.Children().Iter(func(k, v util.T) bool {
key := k.(Value)
child := v.(*typeTreeNode)
tpe := env.getRefRecExtent(child)
// TODO(tsandall): handle non-string keys?
if s, ok := key.(String); ok {
children = append(children, types.NewStaticProperty(string(s), tpe))
}
return false
})
// TODO(tsandall): for now, these objects can have any dynamic properties
// because we don't have schema for base docs. Once schemas are supported
// we can improve this.
return types.NewObject(children, types.NewDynamicProperty(types.S, types.A))
}
func (env *TypeEnv) wrap() *TypeEnv {
cpy := *env
cpy.next = env
cpy.tree = newTypeTree()
return &cpy
}
// typeTreeNode is used to store type information in a tree.
type typeTreeNode struct {
key Value
value types.Type
children *util.HashMap
}
func newTypeTree() *typeTreeNode {
return &typeTreeNode{
key: nil,
value: nil,
children: util.NewHashMap(valueEq, valueHash),
}
}
func (n *typeTreeNode) Child(key Value) *typeTreeNode {
value, ok := n.children.Get(key)
if !ok {
return nil
}
return value.(*typeTreeNode)
}
func (n *typeTreeNode) Children() *util.HashMap {
return n.children
}
func (n *typeTreeNode) Get(path Ref) types.Type {
curr := n
for _, term := range path {
child, ok := curr.children.Get(term.Value)
if !ok {
return nil
}
curr = child.(*typeTreeNode)
}
return curr.Value()
}
func (n *typeTreeNode) Leaf() bool {
return n.value != nil
}
func (n *typeTreeNode) PutOne(key Value, tpe types.Type) {
c, ok := n.children.Get(key)
var child *typeTreeNode
if !ok {
child = newTypeTree()
child.key = key
n.children.Put(key, child)
} else {
child = c.(*typeTreeNode)
}
child.value = tpe
}
func (n *typeTreeNode) Put(path Ref, tpe types.Type) {
curr := n
for _, term := range path {
c, ok := curr.children.Get(term.Value)
var child *typeTreeNode
if !ok {
child = newTypeTree()
child.key = term.Value
curr.children.Put(child.key, child)
} else {
child = c.(*typeTreeNode)
}
curr = child
}
curr.value = tpe
}
func (n *typeTreeNode) Value() types.Type {
return n.value
}
// selectConstant returns the attribute of the type referred to by the term. If
// the attribute type cannot be determined, nil is returned.
func selectConstant(tpe types.Type, term *Term) types.Type {
x, err := JSON(term.Value)
if err == nil {
return types.Select(tpe, x)
}
return nil
}
// selectRef returns the type of the nested attribute referred to by ref. If
// the attribute type cannot be determined, nil is returned. If the ref
// contains vars or refs, then the returned type will be a union of the
// possible types.
func selectRef(tpe types.Type, ref Ref) types.Type {
if tpe == nil || len(ref) == 0 {
return tpe
}
head, tail := ref[0], ref[1:]
switch head.Value.(type) {
case Var, Ref, Array, Object, Set:
return selectRef(types.Values(tpe), tail)
default:
return selectRef(selectConstant(tpe, head), tail)
}
}