blob: bb63010f870018f395337e8e45ff002bdb8b4b52 [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 adt
import (
"fmt"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
)
// TODO: unanswered questions about structural cycles:
//
// 1. When detecting a structural cycle, should we consider this as:
// a) an unevaluated value,
// b) an incomplete error (which does not affect parent validity), or
// c) a special value.
//
// Making it an error is the simplest way to ensure reentrancy is disallowed:
// without an error it would require an additional mechanism to stop reentrancy
// from continuing to process. Even worse, in some cases it may only partially
// evaluate, resulting in unexpected results. For this reason, we are taking
// approach `b` for now.
//
// This has some consequences of how disjunctions are treated though. Consider
//
// list: {
// head: _
// tail: list | null
// }
//
// When making it an error, evaluating the above will result in
//
// list: {
// head: _
// tail: null
// }
//
// because list will result in a structural cycle, and thus an error, it will be
// stripped from the disjunction. This may or may not be a desirable property. A
// nice thing is that it is not required to write `list | *null`. A disadvantage
// is that this is perhaps somewhat inexplicit.
//
// When not making it an error (and simply cease evaluating child arcs upon
// cycle detection), the result would be:
//
// list: {
// head: _
// tail: list | null
// }
//
// In other words, an evaluation would result in a cycle and thus an error.
// Implementations can recognize such cases by having unevaluated arcs. An
// explicit structure cycle marker would probably be less error prone.
//
// Note that in both cases, a reference to list will still use the original
// conjuncts, so the result will be the same for either method in this case.
//
//
// 2. Structural cycle allowance.
//
// Structural cycle detection disallows reentrancy as well. This means one
// cannot use structs for recursive computation. This will probably preclude
// evaluation of some configuration. Given that there is no real alternative
// yet, we could allow structural cycle detection to be optionally disabled.
// An Environment links the parent scopes for identifier lookup to a composite
// node. Each conjunct that make up node in the tree can be associated with
// a different environment (although some conjuncts may share an Environment).
type Environment struct {
Up *Environment
Vertex *Vertex
// DynamicLabel is only set when instantiating a field from a pattern
// constraint. It is used to resolve label references.
DynamicLabel Feature
// TODO(perf): make the following public fields a shareable struct as it
// mostly is going to be the same for child nodes.
// Cyclic indicates a structural cycle was detected for this conjunct or one
// of its ancestors.
Cyclic bool
// Deref keeps track of nodes that should dereference to Vertex. It is used
// for detecting structural cycle.
//
// The detection algorithm is based on Tomabechi's quasi-destructive graph
// unification. This detection requires dependencies to be resolved into
// fully dereferenced vertices. This is not the case in our algorithm:
// the result of evaluating conjuncts is placed into dereferenced vertices
// _after_ they are evaluated, but the Environment still points to the
// non-dereferenced context.
//
// In order to be able to detect structural cycles, we need to ensure that
// at least one node that is part of a cycle in the context in which
// conjunctions are evaluated dereferences correctly.
//
// The only field necessary to detect a structural cycle, however, is
// the Status field of the Vertex. So rather than dereferencing a node
// proper, it is sufficient to copy the Status of the dereferenced nodes
// to these nodes (will always be EvaluatingArcs).
Deref []*Vertex
// Cycles contains vertices for which cycles are detected. It is used
// for tracking self-references within structural cycles.
//
// Unlike Deref, Cycles is not incremented with child nodes.
// TODO: Cycles is always a tail end of Deref, so this can be optimized.
Cycles []*Vertex
cache map[Expr]Value
}
type ID int32
// evalCached is used to look up let expressions. Caching let expressions
// prevents a possible combinatorial explosion.
func (e *Environment) evalCached(c *OpContext, x Expr) Value {
v, ok := e.cache[x]
if !ok {
if e.cache == nil {
e.cache = map[Expr]Value{}
}
env, src := c.e, c.src
c.e, c.src = e, x.Source()
v = c.eval(x)
c.e, c.src = env, src
e.cache[x] = v
}
return v
}
// A Vertex is a node in the value tree. It may be a leaf or internal node.
// It may have arcs to represent elements of a fully evaluated struct or list.
//
// For structs, it only contains definitions and concrete fields.
// optional fields are dropped.
//
// It maintains source information such as a list of conjuncts that contributed
// to the value.
type Vertex struct {
// Parent links to a parent Vertex. This parent should only be used to
// access the parent's Label field to find the relative location within a
// tree.
Parent *Vertex
// Label is the feature leading to this vertex.
Label Feature
// TODO: move the following status fields to a separate struct.
// status indicates the evaluation progress of this vertex.
status VertexStatus
// isData indicates that this Vertex is to be interepreted as data: pattern
// and additional constraints, as well as optional fields, should be
// ignored.
isData bool
// EvalCount keeps track of temporary dereferencing during evaluation.
// If EvalCount > 0, status should be considered to be EvaluatingArcs.
EvalCount int
// SelfCount is used for tracking self-references.
SelfCount int
// Value is the value associated with this vertex. For lists and structs
// this is a sentinel value indicating its kind.
Value Value
// ChildErrors is the collection of all errors of children.
ChildErrors *Bottom
// The parent of nodes can be followed to determine the path within the
// configuration of this node.
// Value Value
Arcs []*Vertex // arcs are sorted in display order.
// Conjuncts lists the structs that ultimately formed this Composite value.
// This includes all selected disjuncts.
//
// This value may be nil, in which case the Arcs are considered to define
// the final value of this Vertex.
Conjuncts []Conjunct
// Structs is a slice of struct literals that contributed to this value.
// This information is used to compute the topological sort of arcs.
Structs []*StructLit
// Closed contains information about how to interpret field labels for the
// various conjuncts with respect to which fields are allowed in this
// Vertex. If allows all fields if it is nil.
// The evaluator will first check existing fields before using this. So for
// simple cases, an Acceptor can always return false to close the Vertex.
Closed Acceptor
}
// VertexStatus indicates the evaluation progress of a Vertex.
type VertexStatus int8
const (
// Unprocessed indicates a Vertex has not been processed before.
// Value must be nil.
Unprocessed VertexStatus = iota
// Evaluating means that the current Vertex is being evaluated. If this is
// encountered it indicates a reference cycle. Value must be nil.
Evaluating
// Partial indicates that the result was only partially evaluated. It will
// need to be fully evaluated to get a complete results.
//
// TODO: this currently requires a renewed computation. Cache the
// nodeContext to allow reusing the computations done so far.
Partial
// EvaluatingArcs indicates that the arcs of the Vertex are currently being
// evaluated. If this is encountered it indicates a structural cycle.
// Value does not have to be nil
EvaluatingArcs
// Finalized means that this node is fully evaluated and that the results
// are save to use without further consideration.
Finalized
)
func (v *Vertex) Status() VertexStatus {
if v.EvalCount > 0 {
return EvaluatingArcs
}
return v.status
}
func (v *Vertex) UpdateStatus(s VertexStatus) {
if v.status > s+1 {
panic(fmt.Sprintf("attempt to regress status from %d to %d", v.Status(), s))
}
if s == Finalized && v.Value == nil {
// panic("not finalized")
}
v.status = s
}
// IsData reports whether v should be interpreted in data mode. In other words,
// it tells whether optional field matching and non-regular fields, like
// definitions and hidden fields, should be ignored.
func (v *Vertex) IsData() bool {
return v.isData || len(v.Conjuncts) == 0
}
// ToDataSingle creates a new Vertex that represents just the regular fields
// of this vertex. Arcs are left untouched.
// It is used by cue.Eval to convert nodes to data on per-node basis.
func (v *Vertex) ToDataSingle() *Vertex {
w := *v
w.isData = true
return &w
}
// ToDataAll returns a new v where v and all its descendents contain only
// the regular fields.
func (v *Vertex) ToDataAll() *Vertex {
arcs := make([]*Vertex, len(v.Arcs))
for i, a := range v.Arcs {
arcs[i] = a.ToDataAll()
}
w := *v
w.Arcs = arcs
w.isData = true
w.Conjuncts = make([]Conjunct, len(v.Conjuncts))
copy(w.Conjuncts, v.Conjuncts)
for i := range w.Conjuncts {
w.Conjuncts[i].CloseID = 0
}
w.Closed = nil
return &w
}
// func (v *Vertex) IsEvaluating() bool {
// return v.Value == cycle
// }
func (v *Vertex) IsErr() bool {
// if v.Status() > Evaluating {
if _, ok := v.Value.(*Bottom); ok {
return true
}
// }
return false
}
func (v *Vertex) Err(c *OpContext, state VertexStatus) *Bottom {
c.Unify(c, v, state)
if b, ok := v.Value.(*Bottom); ok {
return b
}
return nil
}
// func (v *Vertex) Evaluate()
func (v *Vertex) Finalize(c *OpContext) {
if c == nil {
fmt.Println("WOT?")
}
c.Unify(c, v, Finalized)
}
func (v *Vertex) AddErr(ctx *OpContext, b *Bottom) {
v.Value = CombineErrors(nil, v.Value, b)
v.UpdateStatus(Finalized)
}
func (v *Vertex) SetValue(ctx *OpContext, state VertexStatus, value Value) *Bottom {
v.Value = value
v.UpdateStatus(state)
return nil
}
// ToVertex wraps v in a new Vertex, if necessary.
func ToVertex(v Value) *Vertex {
switch x := v.(type) {
case *Vertex:
return x
default:
n := &Vertex{
status: Finalized,
Value: x,
}
n.AddConjunct(MakeRootConjunct(nil, v))
return n
}
}
// Unwrap returns the possibly non-concrete scalar value of v or nil if v is
// a list, struct or of undefined type.
func Unwrap(v Value) Value {
x, ok := v.(*Vertex)
if !ok {
return v
}
switch x.Value.(type) {
case *StructMarker, *ListMarker:
return v
default:
return x.Value
}
}
// Acceptor is a single interface that reports whether feature f is a valid
// field label for this vertex.
//
// TODO: combine this with the StructMarker functionality?
type Acceptor interface {
// Accept reports whether a given field is accepted as output.
// Pass an InvalidLabel to determine whether this is always open.
Accept(ctx *OpContext, f Feature) bool
// MatchAndInsert finds the conjuncts for optional fields, pattern
// constraints, and additional constraints that match f and inserts them in
// arc. Use f is 0 to match all additional constraints only.
MatchAndInsert(c *OpContext, arc *Vertex)
// OptionalTypes returns a bit field with the type of optional constraints
// that are represented by this Acceptor.
OptionalTypes() OptionalType
}
// OptionalType is a bit field of the type of optional constraints in use by an
// Acceptor.
type OptionalType int
const (
HasField OptionalType = 1 << iota // X: T
HasDynamic // (X): T or "\(X)": T
HasPattern // [X]: T
HasAdditional // ...T
IsOpen // Defined for all fields
)
func (v *Vertex) Kind() Kind {
// This is possible when evaluating comprehensions. It is potentially
// not known at this time what the type is.
if v.Value == nil {
return TopKind
}
return v.Value.Kind()
}
func (v *Vertex) OptionalTypes() OptionalType {
switch {
case v.Closed != nil:
return v.Closed.OptionalTypes()
case v.IsList():
return 0
default:
return IsOpen
}
}
func (v *Vertex) IsClosed(ctx *OpContext) bool {
switch x := v.Value.(type) {
case *ListMarker:
// TODO: use one mechanism.
if x.IsOpen {
return false
}
if v.Closed == nil {
return true
}
return !v.Closed.Accept(ctx, InvalidLabel)
case *StructMarker:
if x.NeedClose {
return true
}
if v.Closed == nil {
return false
}
return !v.Closed.Accept(ctx, InvalidLabel)
}
return false
}
func (v *Vertex) Accept(ctx *OpContext, f Feature) bool {
if !v.IsClosed(ctx) || v.Lookup(f) != nil {
return true
}
if v.Closed != nil {
return v.Closed.Accept(ctx, f)
}
return false
}
func (v *Vertex) MatchAndInsert(ctx *OpContext, arc *Vertex) {
if v.Closed == nil {
return
}
if !v.Accept(ctx, arc.Label) {
return
}
v.Closed.MatchAndInsert(ctx, arc)
}
func (v *Vertex) IsList() bool {
_, ok := v.Value.(*ListMarker)
return ok
}
// Lookup returns the Arc with label f if it exists or nil otherwise.
func (v *Vertex) Lookup(f Feature) *Vertex {
for _, a := range v.Arcs {
if a.Label == f {
return a
}
}
return nil
}
// Elems returns the regular elements of a list.
func (v *Vertex) Elems() []*Vertex {
// TODO: add bookkeeping for where list arcs start and end.
a := make([]*Vertex, 0, len(v.Arcs))
for _, x := range v.Arcs {
if x.Label.IsInt() {
a = append(a, x)
}
}
return a
}
// GetArc returns a Vertex for the outgoing arc with label f. It creates and
// ads one if it doesn't yet exist.
func (v *Vertex) GetArc(f Feature) (arc *Vertex, isNew bool) {
arc = v.Lookup(f)
if arc == nil {
arc = &Vertex{Parent: v, Label: f}
v.Arcs = append(v.Arcs, arc)
isNew = true
}
return arc, isNew
}
func (v *Vertex) Source() ast.Node { return nil }
// AddConjunct adds the given Conjuncts to v if it doesn't already exist.
func (v *Vertex) AddConjunct(c Conjunct) *Bottom {
if v.Value != nil {
// This is likely a bug in the evaluator and should not happen.
return &Bottom{Err: errors.Newf(token.NoPos, "cannot add conjunct")}
}
for _, x := range v.Conjuncts {
if x == c {
return nil
}
}
v.Conjuncts = append(v.Conjuncts, c)
return nil
}
func (v *Vertex) AddStructs(a ...*StructLit) {
outer:
for _, s := range a {
for _, t := range v.Structs {
if t == s {
continue outer
}
}
v.Structs = append(v.Structs, s)
}
}
// Path computes the sequence of Features leading from the root to of the
// instance to this Vertex.
func (v *Vertex) Path() []Feature {
return appendPath(nil, v)
}
func appendPath(a []Feature, v *Vertex) []Feature {
if v.Parent == nil {
return a
}
a = appendPath(a, v.Parent)
return append(a, v.Label)
}
func (v *Vertex) appendListArcs(arcs []*Vertex) (err *Bottom) {
for _, a := range arcs {
// TODO(list): BUG this only works if lists do not have definitions
// fields.
label, err := MakeLabel(a.Source(), int64(len(v.Arcs)), IntLabel)
if err != nil {
return &Bottom{Src: a.Source(), Err: err}
}
v.Arcs = append(v.Arcs, &Vertex{
Parent: v,
Label: label,
Conjuncts: a.Conjuncts,
})
}
return nil
}
// An Conjunct is an Environment-Expr pair. The Environment is the starting
// point for reference lookup for any reference contained in X.
type Conjunct struct {
Env *Environment
x Node
// CloseID is a unique number that tracks a group of conjuncts that need
// belong to a single originating definition.
CloseID ID
}
func (c *Conjunct) ID() ID {
return c.CloseID
}
// TODO(perf): replace with composite literal if this helps performance.
// MakeRootConjunct creates a conjunct from the given environment and node.
// It panics if x cannot be used as an expression.
func MakeRootConjunct(env *Environment, x Node) Conjunct {
return MakeConjunct(env, x, 0)
}
func MakeConjunct(env *Environment, x Node, id ID) Conjunct {
if env == nil {
// TODO: better is to pass one.
env = &Environment{}
}
switch x.(type) {
case Expr, interface{ expr() Expr }:
default:
panic(fmt.Sprintf("invalid Node type %T", x))
}
return Conjunct{env, x, id}
}
func (c *Conjunct) Source() ast.Node {
return c.x.Source()
}
func (c *Conjunct) Field() Node {
return c.x
}
func (c *Conjunct) Expr() Expr {
switch x := c.x.(type) {
case Expr:
return x
case interface{ expr() Expr }:
return x.expr()
default:
panic("unreachable")
}
}