blob: 80f6ed8a45d12d6288aea738eefd780584660034 [file] [log] [blame]
// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package eval
// The file implements the majority of the closed struct semantics.
// The data is recorded in the Closed field of a Vertex.
//
// Each vertex has a set of conjuncts that make up the values of the vertex.
// Each Conjunct may originate from various sources, like an embedding, field
// definition or regular value. For the purpose of computing the value, the
// source of the conjunct is irrelevant. The origin does matter, however, if
// for determining whether a field is allowed in a closed struct. The Closed
// field keeps track of the kind of origin for this purpose.
//
// More precisely, the CloseDef struct explains how the conjuncts of an arc
// were combined and define a logical expression on the field sets
// computed for each conjunct.
//
// While evaluating each conjunct, nodeContext keeps track what changes need to
// be made to ClosedDef based on the evaluation of the current conjuncts.
// For instance, if a field references a definition, all other previous
// checks are useless, as the newly referred to definitions define an upper
// bound and will contain all the information that is necessary to determine
// whether a field may be included.
//
// Most of the logic in this file concerns itself with the combination of
// multiple CloseDef values as well as traversing the structure to validate
// whether an arc is allowed. The actual fieldSet logic is in optional.go
// The overal control and use of the functionality in this file is used
// in eval.go.
import (
"cuelang.org/go/internal/core/adt"
)
// acceptor implements adt.Acceptor.
//
// Note that it keeps track of whether it represents a closed struct. An
// acceptor is also used to associate an CloseDef with a Vertex, and not
// all CloseDefs represent a closed struct: a value that contains embeddings may
// eventually turn into a closed struct. Consider
//
// a: {
// b
// d: e: int
// }
// b: d: {
// #A & #B
// }
//
// At the point of evaluating `a`, the struct is not yet closed. However,
// descending into `d` will trigger the inclusion of definitions which in turn
// causes the struct to be closed. At this point, it is important to know that
// `b` originated from an embedding, as otherwise `e` may not be allowed.
//
type acceptor struct {
tree *CloseDef
fields []fieldSet
isClosed bool
isList bool
openList bool
}
func (a *acceptor) Accept(c *adt.OpContext, f adt.Feature) bool {
if a.isList {
return a.openList
}
if !a.isClosed {
return true
}
if f == adt.InvalidLabel {
return false
}
if f.IsInt() {
return a.openList
}
return a.verifyArcAllowed(c, f) == nil
}
func (a *acceptor) MatchAndInsert(c *adt.OpContext, v *adt.Vertex) {
for _, fs := range a.fields {
fs.MatchAndInsert(c, v)
}
}
func (a *acceptor) OptionalTypes() (mask adt.OptionalType) {
for _, f := range a.fields {
mask |= f.OptionalTypes()
}
return mask
}
// A disjunction acceptor represents a disjunction of all possible fields. Note
// that this is never used in evaluation as evaluation stops at incomplete nodes
// and a disjunction is incomplete. When the node is referenced, the original
// conjuncts are used instead.
//
// The value may be used in the API, though, where it may be an argument to
// UnifyAccept.
//
// TODO(perf): it would be sufficient to only implement the Accept method of an
// Acceptor. This could be implemented as an allocation-free wrapper type around
// a Disjunction. This will require a bit more API cleaning, though.
func newDisjunctionAcceptor(x *adt.Disjunction) adt.Acceptor {
tree := &CloseDef{}
sets := []fieldSet{}
for _, d := range x.Values {
if a, _ := d.Closed.(*acceptor); a != nil {
sets = append(sets, a.fields...)
tree.List = append(tree.List, a.tree)
}
}
if len(tree.List) == 0 && len(sets) == 0 {
return nil
}
return &acceptor{tree: tree, fields: sets}
}
// CloseDef defines how individual FieldSets (corresponding to conjuncts)
// combine to determine whether a field is contained in a closed set.
//
// Nodes with a non-empty List and IsAnd is false represent embeddings.
// The ID is the node that contained the embedding in that case.
//
// Nodes with a non-empty List and IsAnd is true represent conjunctions of
// definitions. In this case, a field must be contained in each definition.
//
// If a node has both conjunctions of definitions and embeddings, only the
// former are maintained. Conjunctions of definitions define an upper bound
// of the set of allowed fields in that case and the embeddings will not add
// any value.
type CloseDef struct {
ID uint32
IsAnd bool
List []*CloseDef
}
// isOr reports whether this is a node representing embeddings.
func isOr(c *CloseDef) bool {
return len(c.List) > 0 && !c.IsAnd
}
// updateClosed transforms c into a new node with all non-AND nodes with an
// ID matching one in replace substituted with the replace value.
//
// Vertex only keeps track of a flat list of conjuncts and does not keep track
// of the hierarchy of how these were derived. This function allows rewriting
// a CloseDef tree based on replacement information gathered during evaluation
// of this flat list.
//
func updateClosed(c *CloseDef, replace map[uint32]*CloseDef) *CloseDef { // used in eval.go
switch {
case c == nil:
and := []*CloseDef{}
for _, c := range replace {
and = append(and, c)
}
switch len(and) {
case 0:
case 1:
c = and[0]
default:
c = &CloseDef{IsAnd: true, List: and}
}
// needClose
case len(replace) > 0:
c = updateClosedRec(c, replace)
}
return c
}
func updateClosedRec(c *CloseDef, replace map[uint32]*CloseDef) *CloseDef {
if c == nil {
return nil
}
// If c is a leaf or AND node, replace it outright. If both are an OR node,
// merge the lists.
if len(c.List) == 0 || !c.IsAnd {
if sub := replace[c.ID]; sub != nil {
if isOr(sub) && isOr(c) {
sub.List = append(sub.List, c.List...)
}
return sub
}
}
changed := false
buf := make([]*CloseDef, len(c.List))
k := 0
for _, c := range c.List {
n := updateClosedRec(c, replace)
changed = changed || n != c
if n != nil {
buf[k] = n
k++
}
}
if !changed {
return c
}
if k == 1 {
return buf[0]
}
return &CloseDef{ID: c.ID, IsAnd: c.IsAnd, List: buf[:k]}
}
// UpdateReplace is called after evaluating a conjunct at the top of the arc
// to update the replacement information with the gathered CloseDef info.
func (n *nodeContext) updateReplace(env *adt.Environment) { // used in eval.go
if n.newClose == nil {
return
}
if n.replace == nil {
n.replace = make(map[uint32]*CloseDef)
}
id := uint32(0)
if env != nil {
id = env.CloseID
}
n.replace[id] = updateClose(n.replace[id], n.newClose)
n.newClose = nil
}
// appendList creates a new CloseDef with the elements of the list of orig
// and updated appended. It will take the ID of orig. It does not alter
// either orig or update.
func appendLists(orig, update *CloseDef) *CloseDef {
list := make([]*CloseDef, len(orig.List)+len(update.List))
copy(list[copy(list, orig.List):], update.List)
c := *orig
c.List = list
return &c
}
// updateClose merges update into orig without altering either.
//
// The merge takes into account whether it is an embedding node or not.
// Most notably, if an "And" node is combined with an embedding, the
// embedding information may be discarded.
func updateClose(orig, update *CloseDef) *CloseDef {
switch {
case orig == nil:
return update
case isOr(orig):
if !isOr(update) {
return update
}
return appendLists(orig, update)
case isOr(update):
return orig
case len(orig.List) == 0 && len(update.List) == 0:
return &CloseDef{IsAnd: true, List: []*CloseDef{orig, update}}
case len(orig.List) == 0:
update.List = append(update.List, orig)
return update
default: // isAnd(orig)
return appendLists(orig, update)
}
}
func (n *nodeContext) addAnd(c *CloseDef) { // used in eval.go
switch {
case n.newClose == nil:
n.newClose = c
case isOr(n.newClose):
n.newClose = c
case len(n.newClose.List) == 0:
n.newClose = &CloseDef{
IsAnd: true,
List: []*CloseDef{n.newClose, c},
}
default:
n.newClose.List = append(n.newClose.List, c)
}
}
func (n *nodeContext) addOr(parentID uint32, c *CloseDef) { // used in eval.go
switch {
case n.newClose == nil:
d := &CloseDef{ID: parentID, List: []*CloseDef{{ID: parentID}}}
if c != nil {
d.List = append(d.List, c)
}
n.newClose = d
case isOr(n.newClose):
d := n.newClose
if c != nil {
d.List = append(d.List, c)
}
}
}
// verifyArcAllowed checks whether f is an allowed label within the current
// node. It traverses c considering the "or" semantics of embeddings and the
// "and" semantics of conjunctions. It generates an error if a field is not
// allowed.
func (n *acceptor) verifyArcAllowed(ctx *adt.OpContext, f adt.Feature) *adt.Bottom {
// TODO(perf): this will also generate a message for f == 0. Do something
// more clever and only generate this when it is a user error.
filter := f.IsString() || f == adt.InvalidLabel
if filter && !n.verifyArcRecursive(ctx, n.tree, f) {
label := f.SelectorString(ctx)
return ctx.NewErrf("field `%s` not allowed", label)
}
return nil
}
func (n *acceptor) verifyArcRecursive(ctx *adt.OpContext, c *CloseDef, f adt.Feature) bool {
if len(c.List) == 0 {
return n.verifyDefinition(ctx, c.ID, f)
}
if c.IsAnd {
for _, c := range c.List {
if !n.verifyArcRecursive(ctx, c, f) {
return false
}
}
return true
}
for _, c := range c.List {
if n.verifyArcRecursive(ctx, c, f) {
return true
}
}
return false
}
// verifyDefinition reports whether f is a valid member for any of the fieldSets
// with the same closeID.
func (n *acceptor) verifyDefinition(ctx *adt.OpContext, closeID uint32, f adt.Feature) (ok bool) {
for _, o := range n.fields {
if o.env.CloseID != closeID {
continue
}
if len(o.additional) > 0 || o.isOpen {
return true
}
for _, g := range o.fields {
if f == g.label {
return true
}
}
for _, b := range o.bulk {
if b.check.Match(ctx, f) {
return true
}
}
}
return false
}