rebase: update kubernetes to 1.28.0 in main

updating kubernetes to 1.28.0
in the main repo.

Signed-off-by: Madhu Rajanna <madhupr007@gmail.com>
This commit is contained in:
Madhu Rajanna
2023-08-17 07:15:28 +02:00
committed by mergify[bot]
parent b2fdc269c3
commit ff3e84ad67
706 changed files with 45252 additions and 16346 deletions

View File

@ -11,10 +11,10 @@ go_library(
"activation.go",
"attribute_patterns.go",
"attributes.go",
"coster.go",
"decorators.go",
"dispatcher.go",
"evalstate.go",
"formatting.go",
"interpretable.go",
"interpreter.go",
"optimizations.go",
@ -32,7 +32,7 @@ go_library(
"//common/types/ref:go_default_library",
"//common/types/traits:go_default_library",
"//interpreter/functions:go_default_library",
"@org_golang_google_genproto//googleapis/api/expr/v1alpha1:go_default_library",
"@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_google_protobuf//types/known/durationpb:go_default_library",
"@org_golang_google_protobuf//types/known/structpb:go_default_library",
@ -49,6 +49,7 @@ go_test(
"attributes_test.go",
"interpreter_test.go",
"prune_test.go",
"runtimecost_test.go",
],
embed = [
":go_default_library",
@ -65,7 +66,7 @@ go_test(
"//test:go_default_library",
"//test/proto2pb:go_default_library",
"//test/proto3pb:go_default_library",
"@org_golang_google_genproto//googleapis/api/expr/v1alpha1:go_default_library",
"@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_google_protobuf//types/known/anypb:go_default_library",
],

View File

@ -28,7 +28,7 @@ import (
type Activation interface {
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
ResolveName(name string) (interface{}, bool)
ResolveName(name string) (any, bool)
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
@ -43,23 +43,23 @@ func EmptyActivation() Activation {
// emptyActivation is a variable-free activation.
type emptyActivation struct{}
func (emptyActivation) ResolveName(string) (interface{}, bool) { return nil, false }
func (emptyActivation) Parent() Activation { return nil }
func (emptyActivation) ResolveName(string) (any, bool) { return nil, false }
func (emptyActivation) Parent() Activation { return nil }
// NewActivation returns an activation based on a map-based binding where the map keys are
// expected to be qualified names used with ResolveName calls.
//
// The input `bindings` may either be of type `Activation` or `map[string]interface{}`.
// The input `bindings` may either be of type `Activation` or `map[string]any`.
//
// Lazy bindings may be supplied within the map-based input in either of the following forms:
// - func() interface{}
// - func() any
// - func() ref.Val
//
// The output of the lazy binding will overwrite the variable reference in the internal map.
//
// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using
// the ref.TypeAdapter configured in the environment.
func NewActivation(bindings interface{}) (Activation, error) {
func NewActivation(bindings any) (Activation, error) {
if bindings == nil {
return nil, errors.New("bindings must be non-nil")
}
@ -67,7 +67,7 @@ func NewActivation(bindings interface{}) (Activation, error) {
if isActivation {
return a, nil
}
m, isMap := bindings.(map[string]interface{})
m, isMap := bindings.(map[string]any)
if !isMap {
return nil, fmt.Errorf(
"activation input must be an activation or map[string]interface: got %T",
@ -81,7 +81,7 @@ func NewActivation(bindings interface{}) (Activation, error) {
// Named bindings may lazily supply values by providing a function which accepts no arguments and
// produces an interface value.
type mapActivation struct {
bindings map[string]interface{}
bindings map[string]any
}
// Parent implements the Activation interface method.
@ -90,7 +90,7 @@ func (a *mapActivation) Parent() Activation {
}
// ResolveName implements the Activation interface method.
func (a *mapActivation) ResolveName(name string) (interface{}, bool) {
func (a *mapActivation) ResolveName(name string) (any, bool) {
obj, found := a.bindings[name]
if !found {
return nil, false
@ -100,7 +100,7 @@ func (a *mapActivation) ResolveName(name string) (interface{}, bool) {
obj = fn()
a.bindings[name] = obj
}
fnRaw, isLazy := obj.(func() interface{})
fnRaw, isLazy := obj.(func() any)
if isLazy {
obj = fnRaw()
a.bindings[name] = obj
@ -121,7 +121,7 @@ func (a *hierarchicalActivation) Parent() Activation {
}
// ResolveName implements the Activation interface method.
func (a *hierarchicalActivation) ResolveName(name string) (interface{}, bool) {
func (a *hierarchicalActivation) ResolveName(name string) (any, bool) {
if object, found := a.child.ResolveName(name); found {
return object, found
}
@ -138,8 +138,8 @@ func NewHierarchicalActivation(parent Activation, child Activation) Activation {
// representing field and index operations that should result in a 'types.Unknown' result.
//
// The `bindings` value may be any value type supported by the interpreter.NewActivation call,
// but is typically either an existing Activation or map[string]interface{}.
func NewPartialActivation(bindings interface{},
// but is typically either an existing Activation or map[string]any.
func NewPartialActivation(bindings any,
unknowns ...*AttributePattern) (PartialActivation, error) {
a, err := NewActivation(bindings)
if err != nil {
@ -184,7 +184,7 @@ func (v *varActivation) Parent() Activation {
}
// ResolveName implements the Activation interface method.
func (v *varActivation) ResolveName(name string) (interface{}, bool) {
func (v *varActivation) ResolveName(name string) (any, bool) {
if name == v.name {
return v.val, true
}
@ -194,7 +194,7 @@ func (v *varActivation) ResolveName(name string) (interface{}, bool) {
var (
// pool of var activations to reduce allocations during folds.
varActivationPool = &sync.Pool{
New: func() interface{} {
New: func() any {
return &varActivation{}
},
}

View File

@ -15,8 +15,6 @@
package interpreter
import (
"fmt"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
@ -36,9 +34,9 @@ import (
//
// Examples:
//
// 1. ns.myvar["complex-value"]
// 2. ns.myvar["complex-value"][0]
// 3. ns.myvar["complex-value"].*.name
// 1. ns.myvar["complex-value"]
// 2. ns.myvar["complex-value"][0]
// 3. ns.myvar["complex-value"].*.name
//
// The first example is simple: match an attribute where the variable is 'ns.myvar' with a
// field access on 'complex-value'. The second example expands the match to indicate that only
@ -108,7 +106,7 @@ func (apat *AttributePattern) QualifierPatterns() []*AttributeQualifierPattern {
// AttributeQualifierPattern holds a wildcard or valued qualifier pattern.
type AttributeQualifierPattern struct {
wildcard bool
value interface{}
value any
}
// Matches returns true if the qualifier pattern is a wildcard, or the Qualifier implements the
@ -134,44 +132,44 @@ func (qpat *AttributeQualifierPattern) Matches(q Qualifier) bool {
type qualifierValueEquator interface {
// QualifierValueEquals returns true if the input value is equal to the value held in the
// Qualifier.
QualifierValueEquals(value interface{}) bool
QualifierValueEquals(value any) bool
}
// QualifierValueEquals implementation for boolean qualifiers.
func (q *boolQualifier) QualifierValueEquals(value interface{}) bool {
func (q *boolQualifier) QualifierValueEquals(value any) bool {
bval, ok := value.(bool)
return ok && q.value == bval
}
// QualifierValueEquals implementation for field qualifiers.
func (q *fieldQualifier) QualifierValueEquals(value interface{}) bool {
func (q *fieldQualifier) QualifierValueEquals(value any) bool {
sval, ok := value.(string)
return ok && q.Name == sval
}
// QualifierValueEquals implementation for string qualifiers.
func (q *stringQualifier) QualifierValueEquals(value interface{}) bool {
func (q *stringQualifier) QualifierValueEquals(value any) bool {
sval, ok := value.(string)
return ok && q.value == sval
}
// QualifierValueEquals implementation for int qualifiers.
func (q *intQualifier) QualifierValueEquals(value interface{}) bool {
func (q *intQualifier) QualifierValueEquals(value any) bool {
return numericValueEquals(value, q.celValue)
}
// QualifierValueEquals implementation for uint qualifiers.
func (q *uintQualifier) QualifierValueEquals(value interface{}) bool {
func (q *uintQualifier) QualifierValueEquals(value any) bool {
return numericValueEquals(value, q.celValue)
}
// QualifierValueEquals implementation for double qualifiers.
func (q *doubleQualifier) QualifierValueEquals(value interface{}) bool {
func (q *doubleQualifier) QualifierValueEquals(value any) bool {
return numericValueEquals(value, q.celValue)
}
// numericValueEquals uses CEL equality to determine whether two number values are
func numericValueEquals(value interface{}, celValue ref.Val) bool {
func numericValueEquals(value any, celValue ref.Val) bool {
val := types.DefaultTypeAdapter.NativeToValue(value)
return celValue.Equal(val) == types.True
}
@ -272,13 +270,9 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns(
if err != nil {
return nil, err
}
unk, isUnk := val.(types.Unknown)
if isUnk {
return unk, nil
}
// If this resolution behavior ever changes, new implementations of the
// qualifierValueEquator may be required to handle proper resolution.
qual, err = fac.NewQualifier(nil, qual.ID(), val)
qual, err = fac.NewQualifier(nil, qual.ID(), val, attr.IsOptional())
if err != nil {
return nil, err
}
@ -338,24 +332,10 @@ func (m *attributeMatcher) AddQualifier(qual Qualifier) (Attribute, error) {
return m, nil
}
// Resolve is an implementation of the Attribute interface method which uses the
// attributeMatcher TryResolve implementation rather than the embedded NamespacedAttribute
// Resolve implementation.
func (m *attributeMatcher) Resolve(vars Activation) (interface{}, error) {
obj, found, err := m.TryResolve(vars)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("no such attribute: %v", m.NamespacedAttribute)
}
return obj, nil
}
// TryResolve is an implementation of the NamespacedAttribute interface method which tests
// Resolve is an implementation of the NamespacedAttribute interface method which tests
// for matching unknown attribute patterns and returns types.Unknown if present. Otherwise,
// the standard Resolve logic applies.
func (m *attributeMatcher) TryResolve(vars Activation) (interface{}, bool, error) {
func (m *attributeMatcher) Resolve(vars Activation) (any, error) {
id := m.NamespacedAttribute.ID()
// Bug in how partial activation is resolved, should search parents as well.
partial, isPartial := toPartialActivation(vars)
@ -366,30 +346,23 @@ func (m *attributeMatcher) TryResolve(vars Activation) (interface{}, bool, error
m.CandidateVariableNames(),
m.qualifiers)
if err != nil {
return nil, true, err
return nil, err
}
if unk != nil {
return unk, true, nil
return unk, nil
}
}
return m.NamespacedAttribute.TryResolve(vars)
return m.NamespacedAttribute.Resolve(vars)
}
// Qualify is an implementation of the Qualifier interface method.
func (m *attributeMatcher) Qualify(vars Activation, obj interface{}) (interface{}, error) {
val, err := m.Resolve(vars)
if err != nil {
return nil, err
}
unk, isUnk := val.(types.Unknown)
if isUnk {
return unk, nil
}
qual, err := m.fac.NewQualifier(nil, m.ID(), val)
if err != nil {
return nil, err
}
return qual.Qualify(vars, obj)
func (m *attributeMatcher) Qualify(vars Activation, obj any) (any, error) {
return attrQualify(m.fac, vars, obj, m)
}
// QualifyIfPresent is an implementation of the Qualifier interface method.
func (m *attributeMatcher) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) {
return attrQualifyIfPresent(m.fac, vars, obj, m, presenceOnly)
}
func toPartialActivation(vars Activation) (PartialActivation, bool) {

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +0,0 @@
// Copyright 2020 Google LLC
//
// 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 interpreter
import "math"
// TODO: remove Coster.
// Coster calculates the heuristic cost incurred during evaluation.
// Deprecated: Please migrate cel.EstimateCost, it supports length estimates for input data and cost estimates for
// extension functions.
type Coster interface {
Cost() (min, max int64)
}
// estimateCost returns the heuristic cost interval for the program.
func estimateCost(i interface{}) (min, max int64) {
c, ok := i.(Coster)
if !ok {
return 0, math.MaxInt64
}
return c.Cost()
}

View File

@ -29,7 +29,7 @@ type InterpretableDecorator func(Interpretable) (Interpretable, error)
func decObserveEval(observer EvalObserver) InterpretableDecorator {
return func(i Interpretable) (Interpretable, error) {
switch inst := i.(type) {
case *evalWatch, *evalWatchAttr, *evalWatchConst:
case *evalWatch, *evalWatchAttr, *evalWatchConst, *evalWatchConstructor:
// these instruction are already watching, return straight-away.
return i, nil
case InterpretableAttribute:
@ -42,6 +42,11 @@ func decObserveEval(observer EvalObserver) InterpretableDecorator {
InterpretableConst: inst,
observer: observer,
}, nil
case InterpretableConstructor:
return &evalWatchConstructor{
constructor: inst,
observer: observer,
}, nil
default:
return &evalWatch{
Interpretable: i,
@ -224,8 +229,8 @@ func maybeOptimizeSetMembership(i Interpretable, inlist InterpretableCall) (Inte
valueSet := make(map[ref.Val]ref.Val)
for it.HasNext() == types.True {
elem := it.Next()
if !types.IsPrimitiveType(elem) {
// Note, non-primitive type are not yet supported.
if !types.IsPrimitiveType(elem) || elem.Type() == types.BytesType {
// Note, non-primitive type are not yet supported, and []byte isn't hashable.
return i, nil
}
valueSet[elem] = types.True

View File

@ -0,0 +1,383 @@
// Copyright 2023 Google LLC
//
// 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 interpreter
import (
"errors"
"fmt"
"strconv"
"strings"
"unicode"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
type typeVerifier func(int64, ...*types.TypeValue) (bool, error)
// InterpolateFormattedString checks the syntax and cardinality of any string.format calls present in the expression and reports
// any errors at compile time.
func InterpolateFormattedString(verifier typeVerifier) InterpretableDecorator {
return func(inter Interpretable) (Interpretable, error) {
call, ok := inter.(InterpretableCall)
if !ok {
return inter, nil
}
if call.OverloadID() != "string_format" {
return inter, nil
}
args := call.Args()
if len(args) != 2 {
return nil, fmt.Errorf("wrong number of arguments to string.format (expected 2, got %d)", len(args))
}
fmtStrInter, ok := args[0].(InterpretableConst)
if !ok {
return inter, nil
}
var fmtArgsInter InterpretableConstructor
fmtArgsInter, ok = args[1].(InterpretableConstructor)
if !ok {
return inter, nil
}
if fmtArgsInter.Type() != types.ListType {
// don't necessarily return an error since the list may be DynType
return inter, nil
}
formatStr := fmtStrInter.Value().Value().(string)
initVals := fmtArgsInter.InitVals()
formatCheck := &formatCheck{
args: initVals,
verifier: verifier,
}
// use a placeholder locale, since locale doesn't affect syntax
_, err := ParseFormatString(formatStr, formatCheck, formatCheck, "en_US")
if err != nil {
return nil, err
}
seenArgs := formatCheck.argsRequested
if len(initVals) > seenArgs {
return nil, fmt.Errorf("too many arguments supplied to string.format (expected %d, got %d)", seenArgs, len(initVals))
}
return inter, nil
}
}
type formatCheck struct {
args []Interpretable
argsRequested int
curArgIndex int64
enableCheckArgTypes bool
verifier typeVerifier
}
func (c *formatCheck) String(arg ref.Val, locale string) (string, error) {
valid, err := verifyString(c.args[c.curArgIndex], c.verifier)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps")
}
return "", nil
}
func (c *formatCheck) Decimal(arg ref.Val, locale string) (string, error) {
id := c.args[c.curArgIndex].ID()
valid, err := c.verifier(id, types.IntType, types.UintType)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("integer clause can only be used on integers")
}
return "", nil
}
func (c *formatCheck) Fixed(precision *int) func(ref.Val, string) (string, error) {
return func(arg ref.Val, locale string) (string, error) {
id := c.args[c.curArgIndex].ID()
// we allow StringType since "NaN", "Infinity", and "-Infinity" are also valid values
valid, err := c.verifier(id, types.DoubleType, types.StringType)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("fixed-point clause can only be used on doubles")
}
return "", nil
}
}
func (c *formatCheck) Scientific(precision *int) func(ref.Val, string) (string, error) {
return func(arg ref.Val, locale string) (string, error) {
id := c.args[c.curArgIndex].ID()
valid, err := c.verifier(id, types.DoubleType, types.StringType)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("scientific clause can only be used on doubles")
}
return "", nil
}
}
func (c *formatCheck) Binary(arg ref.Val, locale string) (string, error) {
id := c.args[c.curArgIndex].ID()
valid, err := c.verifier(id, types.IntType, types.UintType, types.BoolType)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("only integers and bools can be formatted as binary")
}
return "", nil
}
func (c *formatCheck) Hex(useUpper bool) func(ref.Val, string) (string, error) {
return func(arg ref.Val, locale string) (string, error) {
id := c.args[c.curArgIndex].ID()
valid, err := c.verifier(id, types.IntType, types.UintType, types.StringType, types.BytesType)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("only integers, byte buffers, and strings can be formatted as hex")
}
return "", nil
}
}
func (c *formatCheck) Octal(arg ref.Val, locale string) (string, error) {
id := c.args[c.curArgIndex].ID()
valid, err := c.verifier(id, types.IntType, types.UintType)
if err != nil {
return "", err
}
if !valid {
return "", errors.New("octal clause can only be used on integers")
}
return "", nil
}
func (c *formatCheck) Arg(index int64) (ref.Val, error) {
c.argsRequested++
c.curArgIndex = index
// return a dummy value - this is immediately passed to back to us
// through one of the FormatCallback functions, so anything will do
return types.Int(0), nil
}
func (c *formatCheck) ArgSize() int64 {
return int64(len(c.args))
}
func verifyString(sub Interpretable, verifier typeVerifier) (bool, error) {
subVerified, err := verifier(sub.ID(),
types.ListType, types.MapType, types.IntType, types.UintType, types.DoubleType,
types.BoolType, types.StringType, types.TimestampType, types.BytesType, types.DurationType, types.TypeType, types.NullType)
if err != nil {
return false, err
}
if !subVerified {
return false, nil
}
con, ok := sub.(InterpretableConstructor)
if ok {
members := con.InitVals()
for _, m := range members {
// recursively verify if we're dealing with a list/map
verified, err := verifyString(m, verifier)
if err != nil {
return false, err
}
if !verified {
return false, nil
}
}
}
return true, nil
}
// FormatStringInterpolator is an interface that allows user-defined behavior
// for formatting clause implementations, as well as argument retrieval.
// Each function is expected to support the appropriate types as laid out in
// the string.format documentation, and to return an error if given an inappropriate type.
type FormatStringInterpolator interface {
// String takes a ref.Val and a string representing the current locale identifier
// and returns the Val formatted as a string, or an error if one occurred.
String(ref.Val, string) (string, error)
// Decimal takes a ref.Val and a string representing the current locale identifier
// and returns the Val formatted as a decimal integer, or an error if one occurred.
Decimal(ref.Val, string) (string, error)
// Fixed takes an int pointer representing precision (or nil if none was given) and
// returns a function operating in a similar manner to String and Decimal, taking a
// ref.Val and locale and returning the appropriate string. A closure is returned
// so precision can be set without needing an additional function call/configuration.
Fixed(*int) func(ref.Val, string) (string, error)
// Scientific functions identically to Fixed, except the string returned from the closure
// is expected to be in scientific notation.
Scientific(*int) func(ref.Val, string) (string, error)
// Binary takes a ref.Val and a string representing the current locale identifier
// and returns the Val formatted as a binary integer, or an error if one occurred.
Binary(ref.Val, string) (string, error)
// Hex takes a boolean that, if true, indicates the hex string output by the returned
// closure should use uppercase letters for A-F.
Hex(bool) func(ref.Val, string) (string, error)
// Octal takes a ref.Val and a string representing the current locale identifier and
// returns the Val formatted in octal, or an error if one occurred.
Octal(ref.Val, string) (string, error)
}
// FormatList is an interface that allows user-defined list-like datatypes to be used
// for formatting clause implementations.
type FormatList interface {
// Arg returns the ref.Val at the given index, or an error if one occurred.
Arg(int64) (ref.Val, error)
// ArgSize returns the length of the argument list.
ArgSize() int64
}
type clauseImpl func(ref.Val, string) (string, error)
// ParseFormatString formats a string according to the string.format syntax, taking the clause implementations
// from the provided FormatCallback and the args from the given FormatList.
func ParseFormatString(formatStr string, callback FormatStringInterpolator, list FormatList, locale string) (string, error) {
i := 0
argIndex := 0
var builtStr strings.Builder
for i < len(formatStr) {
if formatStr[i] == '%' {
if i+1 < len(formatStr) && formatStr[i+1] == '%' {
err := builtStr.WriteByte('%')
if err != nil {
return "", fmt.Errorf("error writing format string: %w", err)
}
i += 2
continue
} else {
argAny, err := list.Arg(int64(argIndex))
if err != nil {
return "", err
}
if i+1 >= len(formatStr) {
return "", errors.New("unexpected end of string")
}
if int64(argIndex) >= list.ArgSize() {
return "", fmt.Errorf("index %d out of range", argIndex)
}
numRead, val, refErr := parseAndFormatClause(formatStr[i:], argAny, callback, list, locale)
if refErr != nil {
return "", refErr
}
_, err = builtStr.WriteString(val)
if err != nil {
return "", fmt.Errorf("error writing format string: %w", err)
}
i += numRead
argIndex++
}
} else {
err := builtStr.WriteByte(formatStr[i])
if err != nil {
return "", fmt.Errorf("error writing format string: %w", err)
}
i++
}
}
return builtStr.String(), nil
}
// parseAndFormatClause parses the format clause at the start of the given string with val, and returns
// how many characters were consumed and the substituted string form of val, or an error if one occurred.
func parseAndFormatClause(formatStr string, val ref.Val, callback FormatStringInterpolator, list FormatList, locale string) (int, string, error) {
i := 1
read, formatter, err := parseFormattingClause(formatStr[i:], callback)
i += read
if err != nil {
return -1, "", fmt.Errorf("could not parse formatting clause: %s", err)
}
valStr, err := formatter(val, locale)
if err != nil {
return -1, "", fmt.Errorf("error during formatting: %s", err)
}
return i, valStr, nil
}
func parseFormattingClause(formatStr string, callback FormatStringInterpolator) (int, clauseImpl, error) {
i := 0
read, precision, err := parsePrecision(formatStr[i:])
i += read
if err != nil {
return -1, nil, fmt.Errorf("error while parsing precision: %w", err)
}
r := rune(formatStr[i])
i++
switch r {
case 's':
return i, callback.String, nil
case 'd':
return i, callback.Decimal, nil
case 'f':
return i, callback.Fixed(precision), nil
case 'e':
return i, callback.Scientific(precision), nil
case 'b':
return i, callback.Binary, nil
case 'x', 'X':
return i, callback.Hex(unicode.IsUpper(r)), nil
case 'o':
return i, callback.Octal, nil
default:
return -1, nil, fmt.Errorf("unrecognized formatting clause \"%c\"", r)
}
}
func parsePrecision(formatStr string) (int, *int, error) {
i := 0
if formatStr[i] != '.' {
return i, nil, nil
}
i++
var buffer strings.Builder
for {
if i >= len(formatStr) {
return -1, nil, errors.New("could not find end of precision specifier")
}
if !isASCIIDigit(rune(formatStr[i])) {
break
}
buffer.WriteByte(formatStr[i])
i++
}
precision, err := strconv.Atoi(buffer.String())
if err != nil {
return -1, nil, fmt.Errorf("error while converting precision to integer: %w", err)
}
return i, &precision, nil
}
func isASCIIDigit(r rune) bool {
return r <= unicode.MaxASCII && unicode.IsDigit(r)
}

View File

@ -58,5 +58,5 @@ type UnaryOp func(value ref.Val) ref.Val
type BinaryOp func(lhs ref.Val, rhs ref.Val) ref.Val
// FunctionOp is a function with accepts zero or more arguments and produces
// an value (as interface{}) or error as a result.
// a value or error as a result.
type FunctionOp func(values ...ref.Val) ref.Val

View File

@ -15,7 +15,7 @@
package interpreter
import (
"math"
"fmt"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/overloads"
@ -64,10 +64,18 @@ type InterpretableAttribute interface {
// Qualify replicates the Attribute.Qualify method to permit extension and interception
// of object qualification.
Qualify(vars Activation, obj interface{}) (interface{}, error)
Qualify(vars Activation, obj any) (any, error)
// QualifyIfPresent qualifies the object if the qualifier is declared or defined on the object.
// The 'presenceOnly' flag indicates that the value is not necessary, just a boolean status as
// to whether the qualifier is present.
QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error)
// IsOptional indicates whether the resulting value is an optional type.
IsOptional() bool
// Resolve returns the value of the Attribute given the current Activation.
Resolve(Activation) (interface{}, error)
Resolve(Activation) (any, error)
}
// InterpretableCall interface for inspecting Interpretable instructions related to function calls.
@ -103,10 +111,8 @@ type InterpretableConstructor interface {
// Core Interpretable implementations used during the program planning phase.
type evalTestOnly struct {
id int64
op Interpretable
field types.String
fieldType *ref.FieldType
id int64
InterpretableAttribute
}
// ID implements the Interpretable interface method.
@ -116,44 +122,55 @@ func (test *evalTestOnly) ID() int64 {
// Eval implements the Interpretable interface method.
func (test *evalTestOnly) Eval(ctx Activation) ref.Val {
// Handle field selection on a proto in the most efficient way possible.
if test.fieldType != nil {
opAttr, ok := test.op.(InterpretableAttribute)
if ok {
opVal, err := opAttr.Resolve(ctx)
if err != nil {
return types.NewErr(err.Error())
}
refVal, ok := opVal.(ref.Val)
if ok {
opVal = refVal.Value()
}
if test.fieldType.IsSet(opVal) {
return types.True
}
return types.False
}
val, err := test.Resolve(ctx)
// Return an error if the resolve step fails
if err != nil {
return types.WrapErr(err)
}
obj := test.op.Eval(ctx)
tester, ok := obj.(traits.FieldTester)
if ok {
return tester.IsSet(test.field)
if optVal, isOpt := val.(*types.Optional); isOpt {
return types.Bool(optVal.HasValue())
}
container, ok := obj.(traits.Container)
if ok {
return container.Contains(test.field)
}
return types.ValOrErr(obj, "invalid type for field selection.")
return test.Adapter().NativeToValue(val)
}
// Cost provides the heuristic cost of a `has(field)` macro. The cost has at least 1 for determining
// if the field exists, apart from the cost of accessing the field.
func (test *evalTestOnly) Cost() (min, max int64) {
min, max = estimateCost(test.op)
min++
max++
return
// AddQualifier appends a qualifier that will always and only perform a presence test.
func (test *evalTestOnly) AddQualifier(q Qualifier) (Attribute, error) {
cq, ok := q.(ConstantQualifier)
if !ok {
return nil, fmt.Errorf("test only expressions must have constant qualifiers: %v", q)
}
return test.InterpretableAttribute.AddQualifier(&testOnlyQualifier{ConstantQualifier: cq})
}
type testOnlyQualifier struct {
ConstantQualifier
}
// Qualify determines whether the test-only qualifier is present on the input object.
func (q *testOnlyQualifier) Qualify(vars Activation, obj any) (any, error) {
out, present, err := q.ConstantQualifier.QualifyIfPresent(vars, obj, true)
if err != nil {
return nil, err
}
if unk, isUnk := out.(types.Unknown); isUnk {
return unk, nil
}
if opt, isOpt := out.(types.Optional); isOpt {
return opt.HasValue(), nil
}
return present, nil
}
// QualifyIfPresent returns whether the target field in the test-only expression is present.
func (q *testOnlyQualifier) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) {
// Only ever test for presence.
return q.ConstantQualifier.QualifyIfPresent(vars, obj, true)
}
// QualifierValueEquals determines whether the test-only constant qualifier equals the input value.
func (q *testOnlyQualifier) QualifierValueEquals(value any) bool {
// The input qualifier will always be of type string
return q.ConstantQualifier.Value().Value() == value
}
// NewConstValue creates a new constant valued Interpretable.
@ -179,11 +196,6 @@ func (cons *evalConst) Eval(ctx Activation) ref.Val {
return cons.val
}
// Cost returns zero for a constant valued Interpretable.
func (cons *evalConst) Cost() (min, max int64) {
return 0, 0
}
// Value implements the InterpretableConst interface method.
func (cons *evalConst) Value() ref.Val {
return cons.val
@ -233,12 +245,6 @@ func (or *evalOr) Eval(ctx Activation) ref.Val {
return types.ValOrErr(rVal, "no such overload")
}
// Cost implements the Coster interface method. The minimum possible cost incurs when the left-hand
// side expr is sufficient in determining the evaluation result.
func (or *evalOr) Cost() (min, max int64) {
return calShortCircuitBinaryOpsCost(or.lhs, or.rhs)
}
type evalAnd struct {
id int64
lhs Interpretable
@ -283,18 +289,6 @@ func (and *evalAnd) Eval(ctx Activation) ref.Val {
return types.ValOrErr(rVal, "no such overload")
}
// Cost implements the Coster interface method. The minimum possible cost incurs when the left-hand
// side expr is sufficient in determining the evaluation result.
func (and *evalAnd) Cost() (min, max int64) {
return calShortCircuitBinaryOpsCost(and.lhs, and.rhs)
}
func calShortCircuitBinaryOpsCost(lhs, rhs Interpretable) (min, max int64) {
lMin, lMax := estimateCost(lhs)
_, rMax := estimateCost(rhs)
return lMin, lMax + rMax + 1
}
type evalEq struct {
id int64
lhs Interpretable
@ -319,11 +313,6 @@ func (eq *evalEq) Eval(ctx Activation) ref.Val {
return types.Equal(lVal, rVal)
}
// Cost implements the Coster interface method.
func (eq *evalEq) Cost() (min, max int64) {
return calExhaustiveBinaryOpsCost(eq.lhs, eq.rhs)
}
// Function implements the InterpretableCall interface method.
func (*evalEq) Function() string {
return operators.Equals
@ -363,11 +352,6 @@ func (ne *evalNe) Eval(ctx Activation) ref.Val {
return types.Bool(types.Equal(lVal, rVal) != types.True)
}
// Cost implements the Coster interface method.
func (ne *evalNe) Cost() (min, max int64) {
return calExhaustiveBinaryOpsCost(ne.lhs, ne.rhs)
}
// Function implements the InterpretableCall interface method.
func (*evalNe) Function() string {
return operators.NotEquals
@ -400,11 +384,6 @@ func (zero *evalZeroArity) Eval(ctx Activation) ref.Val {
return zero.impl()
}
// Cost returns 1 representing the heuristic cost of the function.
func (zero *evalZeroArity) Cost() (min, max int64) {
return 1, 1
}
// Function implements the InterpretableCall interface method.
func (zero *evalZeroArity) Function() string {
return zero.function
@ -456,14 +435,6 @@ func (un *evalUnary) Eval(ctx Activation) ref.Val {
return types.NewErr("no such overload: %s", un.function)
}
// Cost implements the Coster interface method.
func (un *evalUnary) Cost() (min, max int64) {
min, max = estimateCost(un.arg)
min++ // add cost for function
max++
return
}
// Function implements the InterpretableCall interface method.
func (un *evalUnary) Function() string {
return un.function
@ -522,11 +493,6 @@ func (bin *evalBinary) Eval(ctx Activation) ref.Val {
return types.NewErr("no such overload: %s", bin.function)
}
// Cost implements the Coster interface method.
func (bin *evalBinary) Cost() (min, max int64) {
return calExhaustiveBinaryOpsCost(bin.lhs, bin.rhs)
}
// Function implements the InterpretableCall interface method.
func (bin *evalBinary) Function() string {
return bin.function
@ -593,14 +559,6 @@ func (fn *evalVarArgs) Eval(ctx Activation) ref.Val {
return types.NewErr("no such overload: %s", fn.function)
}
// Cost implements the Coster interface method.
func (fn *evalVarArgs) Cost() (min, max int64) {
min, max = sumOfCost(fn.args)
min++ // add cost for function
max++
return
}
// Function implements the InterpretableCall interface method.
func (fn *evalVarArgs) Function() string {
return fn.function
@ -617,9 +575,11 @@ func (fn *evalVarArgs) Args() []Interpretable {
}
type evalList struct {
id int64
elems []Interpretable
adapter ref.TypeAdapter
id int64
elems []Interpretable
optionals []bool
hasOptionals bool
adapter ref.TypeAdapter
}
// ID implements the Interpretable interface method.
@ -629,14 +589,24 @@ func (l *evalList) ID() int64 {
// Eval implements the Interpretable interface method.
func (l *evalList) Eval(ctx Activation) ref.Val {
elemVals := make([]ref.Val, len(l.elems))
elemVals := make([]ref.Val, 0, len(l.elems))
// If any argument is unknown or error early terminate.
for i, elem := range l.elems {
elemVal := elem.Eval(ctx)
if types.IsUnknownOrError(elemVal) {
return elemVal
}
elemVals[i] = elemVal
if l.hasOptionals && l.optionals[i] {
optVal, ok := elemVal.(*types.Optional)
if !ok {
return invalidOptionalElementInit(elemVal)
}
if !optVal.HasValue() {
continue
}
elemVal = optVal.GetValue()
}
elemVals = append(elemVals, elemVal)
}
return l.adapter.NativeToValue(elemVals)
}
@ -649,16 +619,13 @@ func (l *evalList) Type() ref.Type {
return types.ListType
}
// Cost implements the Coster interface method.
func (l *evalList) Cost() (min, max int64) {
return sumOfCost(l.elems)
}
type evalMap struct {
id int64
keys []Interpretable
vals []Interpretable
adapter ref.TypeAdapter
id int64
keys []Interpretable
vals []Interpretable
optionals []bool
hasOptionals bool
adapter ref.TypeAdapter
}
// ID implements the Interpretable interface method.
@ -679,6 +646,17 @@ func (m *evalMap) Eval(ctx Activation) ref.Val {
if types.IsUnknownOrError(valVal) {
return valVal
}
if m.hasOptionals && m.optionals[i] {
optVal, ok := valVal.(*types.Optional)
if !ok {
return invalidOptionalEntryInit(keyVal, valVal)
}
if !optVal.HasValue() {
delete(entries, keyVal)
continue
}
valVal = optVal.GetValue()
}
entries[keyVal] = valVal
}
return m.adapter.NativeToValue(entries)
@ -704,19 +682,14 @@ func (m *evalMap) Type() ref.Type {
return types.MapType
}
// Cost implements the Coster interface method.
func (m *evalMap) Cost() (min, max int64) {
kMin, kMax := sumOfCost(m.keys)
vMin, vMax := sumOfCost(m.vals)
return kMin + vMin, kMax + vMax
}
type evalObj struct {
id int64
typeName string
fields []string
vals []Interpretable
provider ref.TypeProvider
id int64
typeName string
fields []string
vals []Interpretable
optionals []bool
hasOptionals bool
provider ref.TypeProvider
}
// ID implements the Interpretable interface method.
@ -733,6 +706,17 @@ func (o *evalObj) Eval(ctx Activation) ref.Val {
if types.IsUnknownOrError(val) {
return val
}
if o.hasOptionals && o.optionals[i] {
optVal, ok := val.(*types.Optional)
if !ok {
return invalidOptionalEntryInit(field, val)
}
if !optVal.HasValue() {
delete(fieldVals, field)
continue
}
val = optVal.GetValue()
}
fieldVals[field] = val
}
return o.provider.NewValue(o.typeName, fieldVals)
@ -746,21 +730,6 @@ func (o *evalObj) Type() ref.Type {
return types.NewObjectTypeValue(o.typeName)
}
// Cost implements the Coster interface method.
func (o *evalObj) Cost() (min, max int64) {
return sumOfCost(o.vals)
}
func sumOfCost(interps []Interpretable) (min, max int64) {
min, max = 0, 0
for _, in := range interps {
minT, maxT := estimateCost(in)
min += minT
max += maxT
}
return
}
type evalFold struct {
id int64
accuVar string
@ -842,38 +811,6 @@ func (fold *evalFold) Eval(ctx Activation) ref.Val {
return res
}
// Cost implements the Coster interface method.
func (fold *evalFold) Cost() (min, max int64) {
// Compute the cost for evaluating iterRange.
iMin, iMax := estimateCost(fold.iterRange)
// Compute the size of iterRange. If the size depends on the input, return the maximum possible
// cost range.
foldRange := fold.iterRange.Eval(EmptyActivation())
if !foldRange.Type().HasTrait(traits.IterableType) {
return 0, math.MaxInt64
}
var rangeCnt int64
it := foldRange.(traits.Iterable).Iterator()
for it.HasNext() == types.True {
it.Next()
rangeCnt++
}
aMin, aMax := estimateCost(fold.accu)
cMin, cMax := estimateCost(fold.cond)
sMin, sMax := estimateCost(fold.step)
rMin, rMax := estimateCost(fold.result)
if fold.exhaustive {
cMin = cMin * rangeCnt
sMin = sMin * rangeCnt
}
// The cond and step costs are multiplied by size(iterRange). The minimum possible cost incurs
// when the evaluation result can be determined by the first iteration.
return iMin + aMin + cMin + sMin + rMin,
iMax + aMax + cMax*rangeCnt + sMax*rangeCnt + rMax
}
// Optional Interpretable implementations that specialize, subsume, or extend the core evaluation
// plan via decorators.
@ -893,17 +830,15 @@ func (e *evalSetMembership) ID() int64 {
// Eval implements the Interpretable interface method.
func (e *evalSetMembership) Eval(ctx Activation) ref.Val {
val := e.arg.Eval(ctx)
if types.IsUnknownOrError(val) {
return val
}
if ret, found := e.valueSet[val]; found {
return ret
}
return types.False
}
// Cost implements the Coster interface method.
func (e *evalSetMembership) Cost() (min, max int64) {
return estimateCost(e.arg)
}
// evalWatch is an Interpretable implementation that wraps the execution of a given
// expression so that it may observe the computed value and send it to an observer.
type evalWatch struct {
@ -918,15 +853,10 @@ func (e *evalWatch) Eval(ctx Activation) ref.Val {
return val
}
// Cost implements the Coster interface method.
func (e *evalWatch) Cost() (min, max int64) {
return estimateCost(e.Interpretable)
}
// evalWatchAttr describes a watcher of an instAttr Interpretable.
// evalWatchAttr describes a watcher of an InterpretableAttribute Interpretable.
//
// Since the watcher may be selected against at a later stage in program planning, the watcher
// must implement the instAttr interface by proxy.
// must implement the InterpretableAttribute interface by proxy.
type evalWatchAttr struct {
InterpretableAttribute
observer EvalObserver
@ -953,11 +883,6 @@ func (e *evalWatchAttr) AddQualifier(q Qualifier) (Attribute, error) {
return e, err
}
// Cost implements the Coster interface method.
func (e *evalWatchAttr) Cost() (min, max int64) {
return estimateCost(e.InterpretableAttribute)
}
// Eval implements the Interpretable interface method.
func (e *evalWatchAttr) Eval(vars Activation) ref.Val {
val := e.InterpretableAttribute.Eval(vars)
@ -973,17 +898,12 @@ type evalWatchConstQual struct {
adapter ref.TypeAdapter
}
// Cost implements the Coster interface method.
func (e *evalWatchConstQual) Cost() (min, max int64) {
return estimateCost(e.ConstantQualifier)
}
// Qualify observes the qualification of a object via a constant boolean, int, string, or uint.
func (e *evalWatchConstQual) Qualify(vars Activation, obj interface{}) (interface{}, error) {
func (e *evalWatchConstQual) Qualify(vars Activation, obj any) (any, error) {
out, err := e.ConstantQualifier.Qualify(vars, obj)
var val ref.Val
if err != nil {
val = types.NewErr(err.Error())
val = types.WrapErr(err)
} else {
val = e.adapter.NativeToValue(out)
}
@ -991,8 +911,25 @@ func (e *evalWatchConstQual) Qualify(vars Activation, obj interface{}) (interfac
return out, err
}
// QualifyIfPresent conditionally qualifies the variable and only records a value if one is present.
func (e *evalWatchConstQual) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) {
out, present, err := e.ConstantQualifier.QualifyIfPresent(vars, obj, presenceOnly)
var val ref.Val
if err != nil {
val = types.WrapErr(err)
} else if out != nil {
val = e.adapter.NativeToValue(out)
} else if presenceOnly {
val = types.Bool(present)
}
if present || presenceOnly {
e.observer(e.ID(), e.ConstantQualifier, val)
}
return out, present, err
}
// QualifierValueEquals tests whether the incoming value is equal to the qualifying constant.
func (e *evalWatchConstQual) QualifierValueEquals(value interface{}) bool {
func (e *evalWatchConstQual) QualifierValueEquals(value any) bool {
qve, ok := e.ConstantQualifier.(qualifierValueEquator)
return ok && qve.QualifierValueEquals(value)
}
@ -1004,17 +941,12 @@ type evalWatchQual struct {
adapter ref.TypeAdapter
}
// Cost implements the Coster interface method.
func (e *evalWatchQual) Cost() (min, max int64) {
return estimateCost(e.Qualifier)
}
// Qualify observes the qualification of a object via a value computed at runtime.
func (e *evalWatchQual) Qualify(vars Activation, obj interface{}) (interface{}, error) {
func (e *evalWatchQual) Qualify(vars Activation, obj any) (any, error) {
out, err := e.Qualifier.Qualify(vars, obj)
var val ref.Val
if err != nil {
val = types.NewErr(err.Error())
val = types.WrapErr(err)
} else {
val = e.adapter.NativeToValue(out)
}
@ -1022,6 +954,23 @@ func (e *evalWatchQual) Qualify(vars Activation, obj interface{}) (interface{},
return out, err
}
// QualifyIfPresent conditionally qualifies the variable and only records a value if one is present.
func (e *evalWatchQual) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) {
out, present, err := e.Qualifier.QualifyIfPresent(vars, obj, presenceOnly)
var val ref.Val
if err != nil {
val = types.WrapErr(err)
} else if out != nil {
val = e.adapter.NativeToValue(out)
} else if presenceOnly {
val = types.Bool(present)
}
if present || presenceOnly {
e.observer(e.ID(), e.Qualifier, val)
}
return out, present, err
}
// evalWatchConst describes a watcher of an instConst Interpretable.
type evalWatchConst struct {
InterpretableConst
@ -1035,11 +984,6 @@ func (e *evalWatchConst) Eval(vars Activation) ref.Val {
return val
}
// Cost implements the Coster interface method.
func (e *evalWatchConst) Cost() (min, max int64) {
return estimateCost(e.InterpretableConst)
}
// evalExhaustiveOr is just like evalOr, but does not short-circuit argument evaluation.
type evalExhaustiveOr struct {
id int64
@ -1078,12 +1022,7 @@ func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val {
if types.IsError(lVal) {
return lVal
}
return types.ValOrErr(rVal, "no such overload")
}
// Cost implements the Coster interface method.
func (or *evalExhaustiveOr) Cost() (min, max int64) {
return calExhaustiveBinaryOpsCost(or.lhs, or.rhs)
return types.MaybeNoSuchOverloadErr(rVal)
}
// evalExhaustiveAnd is just like evalAnd, but does not short-circuit argument evaluation.
@ -1124,18 +1063,7 @@ func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val {
if types.IsError(lVal) {
return lVal
}
return types.ValOrErr(rVal, "no such overload")
}
// Cost implements the Coster interface method.
func (and *evalExhaustiveAnd) Cost() (min, max int64) {
return calExhaustiveBinaryOpsCost(and.lhs, and.rhs)
}
func calExhaustiveBinaryOpsCost(lhs, rhs Interpretable) (min, max int64) {
lMin, lMax := estimateCost(lhs)
rMin, rMax := estimateCost(rhs)
return lMin + rMin + 1, lMax + rMax + 1
return types.MaybeNoSuchOverloadErr(rVal)
}
// evalExhaustiveConditional is like evalConditional, but does not short-circuit argument
@ -1154,77 +1082,114 @@ func (cond *evalExhaustiveConditional) ID() int64 {
// Eval implements the Interpretable interface method.
func (cond *evalExhaustiveConditional) Eval(ctx Activation) ref.Val {
cVal := cond.attr.expr.Eval(ctx)
tVal, err := cond.attr.truthy.Resolve(ctx)
if err != nil {
return types.NewErr(err.Error())
}
fVal, err := cond.attr.falsy.Resolve(ctx)
if err != nil {
return types.NewErr(err.Error())
}
tVal, tErr := cond.attr.truthy.Resolve(ctx)
fVal, fErr := cond.attr.falsy.Resolve(ctx)
cBool, ok := cVal.(types.Bool)
if !ok {
return types.ValOrErr(cVal, "no such overload")
}
if cBool {
if tErr != nil {
return types.WrapErr(tErr)
}
return cond.adapter.NativeToValue(tVal)
}
if fErr != nil {
return types.WrapErr(fErr)
}
return cond.adapter.NativeToValue(fVal)
}
// Cost implements the Coster interface method.
func (cond *evalExhaustiveConditional) Cost() (min, max int64) {
return cond.attr.Cost()
}
// evalAttr evaluates an Attribute value.
type evalAttr struct {
adapter ref.TypeAdapter
attr Attribute
adapter ref.TypeAdapter
attr Attribute
optional bool
}
var _ InterpretableAttribute = &evalAttr{}
// ID of the attribute instruction.
func (a *evalAttr) ID() int64 {
return a.attr.ID()
}
// AddQualifier implements the instAttr interface method.
// AddQualifier implements the InterpretableAttribute interface method.
func (a *evalAttr) AddQualifier(qual Qualifier) (Attribute, error) {
attr, err := a.attr.AddQualifier(qual)
a.attr = attr
return attr, err
}
// Attr implements the instAttr interface method.
// Attr implements the InterpretableAttribute interface method.
func (a *evalAttr) Attr() Attribute {
return a.attr
}
// Adapter implements the instAttr interface method.
// Adapter implements the InterpretableAttribute interface method.
func (a *evalAttr) Adapter() ref.TypeAdapter {
return a.adapter
}
// Cost implements the Coster interface method.
func (a *evalAttr) Cost() (min, max int64) {
return estimateCost(a.attr)
}
// Eval implements the Interpretable interface method.
func (a *evalAttr) Eval(ctx Activation) ref.Val {
v, err := a.attr.Resolve(ctx)
if err != nil {
return types.NewErr(err.Error())
return types.WrapErr(err)
}
return a.adapter.NativeToValue(v)
}
// Qualify proxies to the Attribute's Qualify method.
func (a *evalAttr) Qualify(ctx Activation, obj interface{}) (interface{}, error) {
func (a *evalAttr) Qualify(ctx Activation, obj any) (any, error) {
return a.attr.Qualify(ctx, obj)
}
// QualifyIfPresent proxies to the Attribute's QualifyIfPresent method.
func (a *evalAttr) QualifyIfPresent(ctx Activation, obj any, presenceOnly bool) (any, bool, error) {
return a.attr.QualifyIfPresent(ctx, obj, presenceOnly)
}
func (a *evalAttr) IsOptional() bool {
return a.optional
}
// Resolve proxies to the Attribute's Resolve method.
func (a *evalAttr) Resolve(ctx Activation) (interface{}, error) {
func (a *evalAttr) Resolve(ctx Activation) (any, error) {
return a.attr.Resolve(ctx)
}
type evalWatchConstructor struct {
constructor InterpretableConstructor
observer EvalObserver
}
// InitVals implements the InterpretableConstructor InitVals function.
func (c *evalWatchConstructor) InitVals() []Interpretable {
return c.constructor.InitVals()
}
// Type implements the InterpretableConstructor Type function.
func (c *evalWatchConstructor) Type() ref.Type {
return c.constructor.Type()
}
// ID implements the Interpretable ID function.
func (c *evalWatchConstructor) ID() int64 {
return c.constructor.ID()
}
// Eval implements the Interpretable Eval function.
func (c *evalWatchConstructor) Eval(ctx Activation) ref.Val {
val := c.constructor.Eval(ctx)
c.observer(c.ID(), c.constructor, val)
return val
}
func invalidOptionalEntryInit(field any, value ref.Val) ref.Val {
return types.NewErr("cannot initialize optional entry '%v' from non-optional value %v", field, value)
}
func invalidOptionalElementInit(value ref.Val) ref.Val {
return types.NewErr("cannot initialize optional list element from non-optional value %v", value)
}

View File

@ -29,19 +29,17 @@ import (
type Interpreter interface {
// NewInterpretable creates an Interpretable from a checked expression and an
// optional list of InterpretableDecorator values.
NewInterpretable(checked *exprpb.CheckedExpr,
decorators ...InterpretableDecorator) (Interpretable, error)
NewInterpretable(checked *exprpb.CheckedExpr, decorators ...InterpretableDecorator) (Interpretable, error)
// NewUncheckedInterpretable returns an Interpretable from a parsed expression
// and an optional list of InterpretableDecorator values.
NewUncheckedInterpretable(expr *exprpb.Expr,
decorators ...InterpretableDecorator) (Interpretable, error)
NewUncheckedInterpretable(expr *exprpb.Expr, decorators ...InterpretableDecorator) (Interpretable, error)
}
// EvalObserver is a functional interface that accepts an expression id and an observed value.
// The id identifies the expression that was evaluated, the programStep is the Interpretable or Qualifier that
// was evaluated and value is the result of the evaluation.
type EvalObserver func(id int64, programStep interface{}, value ref.Val)
type EvalObserver func(id int64, programStep any, value ref.Val)
// Observe constructs a decorator that calls all the provided observers in order after evaluating each Interpretable
// or Qualifier during program evaluation.
@ -49,7 +47,7 @@ func Observe(observers ...EvalObserver) InterpretableDecorator {
if len(observers) == 1 {
return decObserveEval(observers[0])
}
observeFn := func(id int64, programStep interface{}, val ref.Val) {
observeFn := func(id int64, programStep any, val ref.Val) {
for _, observer := range observers {
observer(id, programStep, val)
}
@ -96,7 +94,7 @@ func TrackState(state EvalState) InterpretableDecorator {
// This decorator is not thread-safe, and the EvalState must be reset between Eval()
// calls.
func EvalStateObserver(state EvalState) EvalObserver {
return func(id int64, programStep interface{}, val ref.Val) {
return func(id int64, programStep any, val ref.Val) {
state.SetValue(id, val)
}
}

View File

@ -20,7 +20,6 @@ import (
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter/functions"
@ -189,16 +188,7 @@ func (p *planner) planSelect(expr *exprpb.Expr) (Interpretable, error) {
if err != nil {
return nil, err
}
// Determine the field type if this is a proto message type.
var fieldType *ref.FieldType
opType := p.typeMap[sel.GetOperand().GetId()]
if opType.GetMessageType() != "" {
ft, found := p.provider.FindFieldType(opType.GetMessageType(), sel.GetField())
if found && ft.IsSet != nil && ft.GetFrom != nil {
fieldType = ft
}
}
// If the Select was marked TestOnly, this is a presence test.
//
@ -211,37 +201,31 @@ func (p *planner) planSelect(expr *exprpb.Expr) (Interpretable, error) {
// If a string named 'a.b.c' is declared in the environment and referenced within `has(a.b.c)`,
// it is not clear whether has should error or follow the convention defined for structured
// values.
if sel.TestOnly {
// Return the test only eval expression.
return &evalTestOnly{
id: expr.GetId(),
field: types.String(sel.GetField()),
fieldType: fieldType,
op: op,
}, nil
}
// Build a qualifier.
qual, err := p.attrFactory.NewQualifier(
opType, expr.GetId(), sel.GetField())
if err != nil {
return nil, err
}
// Lastly, create a field selection Interpretable.
// Establish the attribute reference.
attr, isAttr := op.(InterpretableAttribute)
if isAttr {
_, err = attr.AddQualifier(qual)
return attr, err
if !isAttr {
attr, err = p.relativeAttr(op.ID(), op, false)
if err != nil {
return nil, err
}
}
relAttr, err := p.relativeAttr(op.ID(), op)
// Build a qualifier for the attribute.
qual, err := p.attrFactory.NewQualifier(opType, expr.GetId(), sel.GetField(), false)
if err != nil {
return nil, err
}
_, err = relAttr.AddQualifier(qual)
if err != nil {
return nil, err
// Modify the attribute to be test-only.
if sel.GetTestOnly() {
attr = &evalTestOnly{
id: expr.GetId(),
InterpretableAttribute: attr,
}
}
return relAttr, nil
// Append the qualifier on the attribute.
_, err = attr.AddQualifier(qual)
return attr, err
}
// planCall creates a callable Interpretable while specializing for common functions and invocation
@ -286,7 +270,9 @@ func (p *planner) planCall(expr *exprpb.Expr) (Interpretable, error) {
case operators.NotEquals:
return p.planCallNotEqual(expr, args)
case operators.Index:
return p.planCallIndex(expr, args)
return p.planCallIndex(expr, args, false)
case operators.OptSelect, operators.OptIndex:
return p.planCallIndex(expr, args, true)
}
// Otherwise, generate Interpretable calls specialized by argument count.
@ -423,8 +409,7 @@ func (p *planner) planCallVarArgs(expr *exprpb.Expr,
}
// planCallEqual generates an equals (==) Interpretable.
func (p *planner) planCallEqual(expr *exprpb.Expr,
args []Interpretable) (Interpretable, error) {
func (p *planner) planCallEqual(expr *exprpb.Expr, args []Interpretable) (Interpretable, error) {
return &evalEq{
id: expr.GetId(),
lhs: args[0],
@ -433,8 +418,7 @@ func (p *planner) planCallEqual(expr *exprpb.Expr,
}
// planCallNotEqual generates a not equals (!=) Interpretable.
func (p *planner) planCallNotEqual(expr *exprpb.Expr,
args []Interpretable) (Interpretable, error) {
func (p *planner) planCallNotEqual(expr *exprpb.Expr, args []Interpretable) (Interpretable, error) {
return &evalNe{
id: expr.GetId(),
lhs: args[0],
@ -443,8 +427,7 @@ func (p *planner) planCallNotEqual(expr *exprpb.Expr,
}
// planCallLogicalAnd generates a logical and (&&) Interpretable.
func (p *planner) planCallLogicalAnd(expr *exprpb.Expr,
args []Interpretable) (Interpretable, error) {
func (p *planner) planCallLogicalAnd(expr *exprpb.Expr, args []Interpretable) (Interpretable, error) {
return &evalAnd{
id: expr.GetId(),
lhs: args[0],
@ -453,8 +436,7 @@ func (p *planner) planCallLogicalAnd(expr *exprpb.Expr,
}
// planCallLogicalOr generates a logical or (||) Interpretable.
func (p *planner) planCallLogicalOr(expr *exprpb.Expr,
args []Interpretable) (Interpretable, error) {
func (p *planner) planCallLogicalOr(expr *exprpb.Expr, args []Interpretable) (Interpretable, error) {
return &evalOr{
id: expr.GetId(),
lhs: args[0],
@ -463,10 +445,8 @@ func (p *planner) planCallLogicalOr(expr *exprpb.Expr,
}
// planCallConditional generates a conditional / ternary (c ? t : f) Interpretable.
func (p *planner) planCallConditional(expr *exprpb.Expr,
args []Interpretable) (Interpretable, error) {
func (p *planner) planCallConditional(expr *exprpb.Expr, args []Interpretable) (Interpretable, error) {
cond := args[0]
t := args[1]
var tAttr Attribute
truthyAttr, isTruthyAttr := t.(InterpretableAttribute)
@ -493,48 +473,54 @@ func (p *planner) planCallConditional(expr *exprpb.Expr,
// planCallIndex either extends an attribute with the argument to the index operation, or creates
// a relative attribute based on the return of a function call or operation.
func (p *planner) planCallIndex(expr *exprpb.Expr,
args []Interpretable) (Interpretable, error) {
func (p *planner) planCallIndex(expr *exprpb.Expr, args []Interpretable, optional bool) (Interpretable, error) {
op := args[0]
ind := args[1]
opAttr, err := p.relativeAttr(op.ID(), op)
if err != nil {
return nil, err
}
opType := p.typeMap[expr.GetCallExpr().GetTarget().GetId()]
indConst, isIndConst := ind.(InterpretableConst)
if isIndConst {
qual, err := p.attrFactory.NewQualifier(
opType, expr.GetId(), indConst.Value())
// Establish the attribute reference.
var err error
attr, isAttr := op.(InterpretableAttribute)
if !isAttr {
attr, err = p.relativeAttr(op.ID(), op, false)
if err != nil {
return nil, err
}
_, err = opAttr.AddQualifier(qual)
return opAttr, err
}
indAttr, isIndAttr := ind.(InterpretableAttribute)
if isIndAttr {
qual, err := p.attrFactory.NewQualifier(
opType, expr.GetId(), indAttr)
if err != nil {
return nil, err
}
_, err = opAttr.AddQualifier(qual)
return opAttr, err
// Construct the qualifier type.
var qual Qualifier
switch ind := ind.(type) {
case InterpretableConst:
qual, err = p.attrFactory.NewQualifier(opType, expr.GetId(), ind.Value(), optional)
case InterpretableAttribute:
qual, err = p.attrFactory.NewQualifier(opType, expr.GetId(), ind, optional)
default:
qual, err = p.relativeAttr(expr.GetId(), ind, optional)
}
indQual, err := p.relativeAttr(expr.GetId(), ind)
if err != nil {
return nil, err
}
_, err = opAttr.AddQualifier(indQual)
return opAttr, err
// Add the qualifier to the attribute
_, err = attr.AddQualifier(qual)
return attr, err
}
// planCreateList generates a list construction Interpretable.
func (p *planner) planCreateList(expr *exprpb.Expr) (Interpretable, error) {
list := expr.GetListExpr()
elems := make([]Interpretable, len(list.GetElements()))
for i, elem := range list.GetElements() {
optionalIndices := list.GetOptionalIndices()
elements := list.GetElements()
optionals := make([]bool, len(elements))
for _, index := range optionalIndices {
if index < 0 || index >= int32(len(elements)) {
return nil, fmt.Errorf("optional index %d out of element bounds [0, %d]", index, len(elements))
}
optionals[index] = true
}
elems := make([]Interpretable, len(elements))
for i, elem := range elements {
elemVal, err := p.Plan(elem)
if err != nil {
return nil, err
@ -542,9 +528,11 @@ func (p *planner) planCreateList(expr *exprpb.Expr) (Interpretable, error) {
elems[i] = elemVal
}
return &evalList{
id: expr.GetId(),
elems: elems,
adapter: p.adapter,
id: expr.GetId(),
elems: elems,
optionals: optionals,
hasOptionals: len(optionals) != 0,
adapter: p.adapter,
}, nil
}
@ -555,6 +543,7 @@ func (p *planner) planCreateStruct(expr *exprpb.Expr) (Interpretable, error) {
return p.planCreateObj(expr)
}
entries := str.GetEntries()
optionals := make([]bool, len(entries))
keys := make([]Interpretable, len(entries))
vals := make([]Interpretable, len(entries))
for i, entry := range entries {
@ -569,23 +558,27 @@ func (p *planner) planCreateStruct(expr *exprpb.Expr) (Interpretable, error) {
return nil, err
}
vals[i] = valVal
optionals[i] = entry.GetOptionalEntry()
}
return &evalMap{
id: expr.GetId(),
keys: keys,
vals: vals,
adapter: p.adapter,
id: expr.GetId(),
keys: keys,
vals: vals,
optionals: optionals,
hasOptionals: len(optionals) != 0,
adapter: p.adapter,
}, nil
}
// planCreateObj generates an object construction Interpretable.
func (p *planner) planCreateObj(expr *exprpb.Expr) (Interpretable, error) {
obj := expr.GetStructExpr()
typeName, defined := p.resolveTypeName(obj.MessageName)
typeName, defined := p.resolveTypeName(obj.GetMessageName())
if !defined {
return nil, fmt.Errorf("unknown type: %s", typeName)
return nil, fmt.Errorf("unknown type: %s", obj.GetMessageName())
}
entries := obj.GetEntries()
optionals := make([]bool, len(entries))
fields := make([]string, len(entries))
vals := make([]Interpretable, len(entries))
for i, entry := range entries {
@ -595,13 +588,16 @@ func (p *planner) planCreateObj(expr *exprpb.Expr) (Interpretable, error) {
return nil, err
}
vals[i] = val
optionals[i] = entry.GetOptionalEntry()
}
return &evalObj{
id: expr.GetId(),
typeName: typeName,
fields: fields,
vals: vals,
provider: p.provider,
id: expr.GetId(),
typeName: typeName,
fields: fields,
vals: vals,
optionals: optionals,
hasOptionals: len(optionals) != 0,
provider: p.provider,
}, nil
}
@ -753,14 +749,18 @@ func (p *planner) resolveFunction(expr *exprpb.Expr) (*exprpb.Expr, string, stri
return target, fnName, ""
}
func (p *planner) relativeAttr(id int64, eval Interpretable) (InterpretableAttribute, error) {
// relativeAttr indicates that the attribute in this case acts as a qualifier and as such needs to
// be observed to ensure that it's evaluation value is properly recorded for state tracking.
func (p *planner) relativeAttr(id int64, eval Interpretable, opt bool) (InterpretableAttribute, error) {
eAttr, ok := eval.(InterpretableAttribute)
if !ok {
eAttr = &evalAttr{
adapter: p.adapter,
attr: p.attrFactory.RelativeAttribute(id, eval),
adapter: p.adapter,
attr: p.attrFactory.RelativeAttribute(id, eval),
optional: opt,
}
}
// This looks like it should either decorate the new evalAttr node, or early return the InterpretableAttribute
decAttr, err := p.decorate(eAttr, nil)
if err != nil {
return nil, err

View File

@ -16,6 +16,7 @@ package interpreter
import (
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/overloads"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
@ -26,6 +27,7 @@ import (
type astPruner struct {
expr *exprpb.Expr
macroCalls map[int64]*exprpb.Expr
state EvalState
nextExprID int64
}
@ -65,13 +67,22 @@ type astPruner struct {
// compiled and constant folded expressions, but is not willing to constant
// fold(and thus cache results of) some external calls, then they can prepare
// the overloads accordingly.
func PruneAst(expr *exprpb.Expr, state EvalState) *exprpb.Expr {
func PruneAst(expr *exprpb.Expr, macroCalls map[int64]*exprpb.Expr, state EvalState) *exprpb.ParsedExpr {
pruneState := NewEvalState()
for _, id := range state.IDs() {
v, _ := state.Value(id)
pruneState.SetValue(id, v)
}
pruner := &astPruner{
expr: expr,
state: state,
nextExprID: 1}
newExpr, _ := pruner.prune(expr)
return newExpr
macroCalls: macroCalls,
state: pruneState,
nextExprID: getMaxID(expr)}
newExpr, _ := pruner.maybePrune(expr)
return &exprpb.ParsedExpr{
Expr: newExpr,
SourceInfo: &exprpb.SourceInfo{MacroCalls: pruner.macroCalls},
}
}
func (p *astPruner) createLiteral(id int64, val *exprpb.Constant) *exprpb.Expr {
@ -84,28 +95,50 @@ func (p *astPruner) createLiteral(id int64, val *exprpb.Constant) *exprpb.Expr {
}
func (p *astPruner) maybeCreateLiteral(id int64, val ref.Val) (*exprpb.Expr, bool) {
switch val.Type() {
case types.BoolType:
switch v := val.(type) {
case types.Bool:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_BoolValue{BoolValue: val.Value().(bool)}}), true
case types.IntType:
&exprpb.Constant{ConstantKind: &exprpb.Constant_BoolValue{BoolValue: bool(v)}}), true
case types.Bytes:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_Int64Value{Int64Value: val.Value().(int64)}}), true
case types.UintType:
&exprpb.Constant{ConstantKind: &exprpb.Constant_BytesValue{BytesValue: []byte(v)}}), true
case types.Double:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_Uint64Value{Uint64Value: val.Value().(uint64)}}), true
case types.StringType:
&exprpb.Constant{ConstantKind: &exprpb.Constant_DoubleValue{DoubleValue: float64(v)}}), true
case types.Duration:
p.state.SetValue(id, val)
durationString := string(v.ConvertToType(types.StringType).(types.String))
return &exprpb.Expr{
Id: id,
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: &exprpb.Expr_Call{
Function: overloads.TypeConvertDuration,
Args: []*exprpb.Expr{
p.createLiteral(p.nextID(),
&exprpb.Constant{ConstantKind: &exprpb.Constant_StringValue{StringValue: durationString}}),
},
},
},
}, true
case types.Int:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_StringValue{StringValue: val.Value().(string)}}), true
case types.DoubleType:
&exprpb.Constant{ConstantKind: &exprpb.Constant_Int64Value{Int64Value: int64(v)}}), true
case types.Uint:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_DoubleValue{DoubleValue: val.Value().(float64)}}), true
case types.BytesType:
&exprpb.Constant{ConstantKind: &exprpb.Constant_Uint64Value{Uint64Value: uint64(v)}}), true
case types.String:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_BytesValue{BytesValue: val.Value().([]byte)}}), true
case types.NullType:
&exprpb.Constant{ConstantKind: &exprpb.Constant_StringValue{StringValue: string(v)}}), true
case types.Null:
p.state.SetValue(id, val)
return p.createLiteral(id,
&exprpb.Constant{ConstantKind: &exprpb.Constant_NullValue{NullValue: val.Value().(structpb.NullValue)}}), true
&exprpb.Constant{ConstantKind: &exprpb.Constant_NullValue{NullValue: v.Value().(structpb.NullValue)}}), true
}
// Attempt to build a list literal.
@ -123,6 +156,7 @@ func (p *astPruner) maybeCreateLiteral(id int64, val ref.Val) (*exprpb.Expr, boo
}
elemExprs[i] = elemExpr
}
p.state.SetValue(id, val)
return &exprpb.Expr{
Id: id,
ExprKind: &exprpb.Expr_ListExpr{
@ -162,6 +196,7 @@ func (p *astPruner) maybeCreateLiteral(id int64, val ref.Val) (*exprpb.Expr, boo
entries[i] = entry
i++
}
p.state.SetValue(id, val)
return &exprpb.Expr{
Id: id,
ExprKind: &exprpb.Expr_StructExpr{
@ -177,70 +212,147 @@ func (p *astPruner) maybeCreateLiteral(id int64, val ref.Val) (*exprpb.Expr, boo
return nil, false
}
func (p *astPruner) maybePruneAndOr(node *exprpb.Expr) (*exprpb.Expr, bool) {
if !p.existsWithUnknownValue(node.GetId()) {
func (p *astPruner) maybePruneOptional(elem *exprpb.Expr) (*exprpb.Expr, bool) {
elemVal, found := p.value(elem.GetId())
if found && elemVal.Type() == types.OptionalType {
opt := elemVal.(*types.Optional)
if !opt.HasValue() {
return nil, true
}
if newElem, pruned := p.maybeCreateLiteral(elem.GetId(), opt.GetValue()); pruned {
return newElem, true
}
}
return elem, false
}
func (p *astPruner) maybePruneIn(node *exprpb.Expr) (*exprpb.Expr, bool) {
// elem in list
call := node.GetCallExpr()
val, exists := p.maybeValue(call.GetArgs()[1].GetId())
if !exists {
return nil, false
}
if sz, ok := val.(traits.Sizer); ok && sz.Size() == types.IntZero {
return p.maybeCreateLiteral(node.GetId(), types.False)
}
return nil, false
}
func (p *astPruner) maybePruneLogicalNot(node *exprpb.Expr) (*exprpb.Expr, bool) {
call := node.GetCallExpr()
arg := call.GetArgs()[0]
val, exists := p.maybeValue(arg.GetId())
if !exists {
return nil, false
}
if b, ok := val.(types.Bool); ok {
return p.maybeCreateLiteral(node.GetId(), !b)
}
return nil, false
}
func (p *astPruner) maybePruneOr(node *exprpb.Expr) (*exprpb.Expr, bool) {
call := node.GetCallExpr()
// We know result is unknown, so we have at least one unknown arg
// and if one side is a known value, we know we can ignore it.
if p.existsWithKnownValue(call.Args[0].GetId()) {
return call.Args[1], true
if v, exists := p.maybeValue(call.GetArgs()[0].GetId()); exists {
if v == types.True {
return p.maybeCreateLiteral(node.GetId(), types.True)
}
return call.GetArgs()[1], true
}
if p.existsWithKnownValue(call.Args[1].GetId()) {
return call.Args[0], true
if v, exists := p.maybeValue(call.GetArgs()[1].GetId()); exists {
if v == types.True {
return p.maybeCreateLiteral(node.GetId(), types.True)
}
return call.GetArgs()[0], true
}
return nil, false
}
func (p *astPruner) maybePruneAnd(node *exprpb.Expr) (*exprpb.Expr, bool) {
call := node.GetCallExpr()
// We know result is unknown, so we have at least one unknown arg
// and if one side is a known value, we know we can ignore it.
if v, exists := p.maybeValue(call.GetArgs()[0].GetId()); exists {
if v == types.False {
return p.maybeCreateLiteral(node.GetId(), types.False)
}
return call.GetArgs()[1], true
}
if v, exists := p.maybeValue(call.GetArgs()[1].GetId()); exists {
if v == types.False {
return p.maybeCreateLiteral(node.GetId(), types.False)
}
return call.GetArgs()[0], true
}
return nil, false
}
func (p *astPruner) maybePruneConditional(node *exprpb.Expr) (*exprpb.Expr, bool) {
if !p.existsWithUnknownValue(node.GetId()) {
return nil, false
}
call := node.GetCallExpr()
condVal, condValueExists := p.value(call.Args[0].GetId())
if !condValueExists || types.IsUnknownOrError(condVal) {
cond, exists := p.maybeValue(call.GetArgs()[0].GetId())
if !exists {
return nil, false
}
if condVal.Value().(bool) {
return call.Args[1], true
if cond.Value().(bool) {
return call.GetArgs()[1], true
}
return call.Args[2], true
return call.GetArgs()[2], true
}
func (p *astPruner) maybePruneFunction(node *exprpb.Expr) (*exprpb.Expr, bool) {
if _, exists := p.value(node.GetId()); !exists {
return nil, false
}
call := node.GetCallExpr()
if call.Function == operators.LogicalOr || call.Function == operators.LogicalAnd {
return p.maybePruneAndOr(node)
if call.Function == operators.LogicalOr {
return p.maybePruneOr(node)
}
if call.Function == operators.LogicalAnd {
return p.maybePruneAnd(node)
}
if call.Function == operators.Conditional {
return p.maybePruneConditional(node)
}
if call.Function == operators.In {
return p.maybePruneIn(node)
}
if call.Function == operators.LogicalNot {
return p.maybePruneLogicalNot(node)
}
return nil, false
}
func (p *astPruner) maybePrune(node *exprpb.Expr) (*exprpb.Expr, bool) {
return p.prune(node)
}
func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
if node == nil {
return node, false
}
val, valueExists := p.value(node.GetId())
if valueExists && !types.IsUnknownOrError(val) {
val, valueExists := p.maybeValue(node.GetId())
if valueExists {
if newNode, ok := p.maybeCreateLiteral(node.GetId(), val); ok {
delete(p.macroCalls, node.GetId())
return newNode, true
}
}
if macro, found := p.macroCalls[node.GetId()]; found {
// prune the expression in terms of the macro call instead of the expanded form.
if newMacro, pruned := p.prune(macro); pruned {
p.macroCalls[node.GetId()] = newMacro
}
}
// We have either an unknown/error value, or something we don't want to
// transform, or expression was not evaluated. If possible, drill down
// more.
switch node.GetExprKind().(type) {
case *exprpb.Expr_SelectExpr:
if operand, pruned := p.prune(node.GetSelectExpr().GetOperand()); pruned {
if operand, pruned := p.maybePrune(node.GetSelectExpr().GetOperand()); pruned {
return &exprpb.Expr{
Id: node.GetId(),
ExprKind: &exprpb.Expr_SelectExpr{
@ -253,10 +365,6 @@ func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
}, true
}
case *exprpb.Expr_CallExpr:
if newExpr, pruned := p.maybePruneFunction(node); pruned {
newExpr, _ = p.prune(newExpr)
return newExpr, true
}
var prunedCall bool
call := node.GetCallExpr()
args := call.GetArgs()
@ -268,40 +376,75 @@ func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
}
for i, arg := range args {
newArgs[i] = arg
if newArg, prunedArg := p.prune(arg); prunedArg {
if newArg, prunedArg := p.maybePrune(arg); prunedArg {
prunedCall = true
newArgs[i] = newArg
}
}
if newTarget, prunedTarget := p.prune(call.GetTarget()); prunedTarget {
if newTarget, prunedTarget := p.maybePrune(call.GetTarget()); prunedTarget {
prunedCall = true
newCall.Target = newTarget
}
newNode := &exprpb.Expr{
Id: node.GetId(),
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: newCall,
},
}
if newExpr, pruned := p.maybePruneFunction(newNode); pruned {
newExpr, _ = p.maybePrune(newExpr)
return newExpr, true
}
if prunedCall {
return &exprpb.Expr{
Id: node.GetId(),
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: newCall,
},
}, true
return newNode, true
}
case *exprpb.Expr_ListExpr:
elems := node.GetListExpr().GetElements()
newElems := make([]*exprpb.Expr, len(elems))
optIndices := node.GetListExpr().GetOptionalIndices()
optIndexMap := map[int32]bool{}
for _, i := range optIndices {
optIndexMap[i] = true
}
newOptIndexMap := make(map[int32]bool, len(optIndexMap))
newElems := make([]*exprpb.Expr, 0, len(elems))
var prunedList bool
prunedIdx := 0
for i, elem := range elems {
newElems[i] = elem
if newElem, prunedElem := p.prune(elem); prunedElem {
newElems[i] = newElem
prunedList = true
_, isOpt := optIndexMap[int32(i)]
if isOpt {
newElem, pruned := p.maybePruneOptional(elem)
if pruned {
prunedList = true
if newElem != nil {
newElems = append(newElems, newElem)
prunedIdx++
}
continue
}
newOptIndexMap[int32(prunedIdx)] = true
}
if newElem, prunedElem := p.maybePrune(elem); prunedElem {
newElems = append(newElems, newElem)
prunedList = true
} else {
newElems = append(newElems, elem)
}
prunedIdx++
}
optIndices = make([]int32, len(newOptIndexMap))
idx := 0
for i := range newOptIndexMap {
optIndices[idx] = i
idx++
}
if prunedList {
return &exprpb.Expr{
Id: node.GetId(),
ExprKind: &exprpb.Expr_ListExpr{
ListExpr: &exprpb.Expr_CreateList{
Elements: newElems,
Elements: newElems,
OptionalIndices: optIndices,
},
},
}, true
@ -313,8 +456,8 @@ func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
newEntries := make([]*exprpb.Expr_CreateStruct_Entry, len(entries))
for i, entry := range entries {
newEntries[i] = entry
newKey, prunedKey := p.prune(entry.GetMapKey())
newValue, prunedValue := p.prune(entry.GetValue())
newKey, prunedKey := p.maybePrune(entry.GetMapKey())
newValue, prunedValue := p.maybePrune(entry.GetValue())
if !prunedKey && !prunedValue {
continue
}
@ -331,6 +474,7 @@ func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
MapKey: newKey,
}
}
newEntry.OptionalEntry = entry.GetOptionalEntry()
newEntries[i] = newEntry
}
if prunedStruct {
@ -344,27 +488,6 @@ func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
},
}, true
}
case *exprpb.Expr_ComprehensionExpr:
compre := node.GetComprehensionExpr()
// Only the range of the comprehension is pruned since the state tracking only records
// the last iteration of the comprehension and not each step in the evaluation which
// means that the any residuals computed in between might be inaccurate.
if newRange, pruned := p.prune(compre.GetIterRange()); pruned {
return &exprpb.Expr{
Id: node.GetId(),
ExprKind: &exprpb.Expr_ComprehensionExpr{
ComprehensionExpr: &exprpb.Expr_Comprehension{
IterVar: compre.GetIterVar(),
IterRange: newRange,
AccuVar: compre.GetAccuVar(),
AccuInit: compre.GetAccuInit(),
LoopCondition: compre.GetLoopCondition(),
LoopStep: compre.GetLoopStep(),
Result: compre.GetResult(),
},
},
}, true
}
}
return node, false
}
@ -374,24 +497,82 @@ func (p *astPruner) value(id int64) (ref.Val, bool) {
return val, (found && val != nil)
}
func (p *astPruner) existsWithUnknownValue(id int64) bool {
val, valueExists := p.value(id)
return valueExists && types.IsUnknown(val)
}
func (p *astPruner) existsWithKnownValue(id int64) bool {
val, valueExists := p.value(id)
return valueExists && !types.IsUnknown(val)
func (p *astPruner) maybeValue(id int64) (ref.Val, bool) {
val, found := p.value(id)
if !found || types.IsUnknownOrError(val) {
return nil, false
}
return val, true
}
func (p *astPruner) nextID() int64 {
for {
_, found := p.state.Value(p.nextExprID)
if !found {
next := p.nextExprID
p.nextExprID++
return next
}
p.nextExprID++
next := p.nextExprID
p.nextExprID++
return next
}
type astVisitor struct {
// visitEntry is called on every expr node, including those within a map/struct entry.
visitExpr func(expr *exprpb.Expr)
// visitEntry is called before entering the key, value of a map/struct entry.
visitEntry func(entry *exprpb.Expr_CreateStruct_Entry)
}
func getMaxID(expr *exprpb.Expr) int64 {
maxID := int64(1)
visit(expr, maxIDVisitor(&maxID))
return maxID
}
func maxIDVisitor(maxID *int64) astVisitor {
return astVisitor{
visitExpr: func(e *exprpb.Expr) {
if e.GetId() >= *maxID {
*maxID = e.GetId() + 1
}
},
visitEntry: func(e *exprpb.Expr_CreateStruct_Entry) {
if e.GetId() >= *maxID {
*maxID = e.GetId() + 1
}
},
}
}
func visit(expr *exprpb.Expr, visitor astVisitor) {
exprs := []*exprpb.Expr{expr}
for len(exprs) != 0 {
e := exprs[0]
visitor.visitExpr(e)
exprs = exprs[1:]
switch e.GetExprKind().(type) {
case *exprpb.Expr_SelectExpr:
exprs = append(exprs, e.GetSelectExpr().GetOperand())
case *exprpb.Expr_CallExpr:
call := e.GetCallExpr()
if call.GetTarget() != nil {
exprs = append(exprs, call.GetTarget())
}
exprs = append(exprs, call.GetArgs()...)
case *exprpb.Expr_ComprehensionExpr:
compre := e.GetComprehensionExpr()
exprs = append(exprs,
compre.GetIterRange(),
compre.GetAccuInit(),
compre.GetLoopCondition(),
compre.GetLoopStep(),
compre.GetResult())
case *exprpb.Expr_ListExpr:
list := e.GetListExpr()
exprs = append(exprs, list.GetElements()...)
case *exprpb.Expr_StructExpr:
for _, entry := range e.GetStructExpr().GetEntries() {
visitor.visitEntry(entry)
if entry.GetMapKey() != nil {
exprs = append(exprs, entry.GetMapKey())
}
exprs = append(exprs, entry.GetValue())
}
}
}
}

View File

@ -36,7 +36,7 @@ type ActualCostEstimator interface {
// CostObserver provides an observer that tracks runtime cost.
func CostObserver(tracker *CostTracker) EvalObserver {
observer := func(id int64, programStep interface{}, val ref.Val) {
observer := func(id int64, programStep any, val ref.Val) {
switch t := programStep.(type) {
case ConstantQualifier:
// TODO: Push identifiers on to the stack before observing constant qualifiers that apply to them
@ -53,6 +53,11 @@ func CostObserver(tracker *CostTracker) EvalObserver {
tracker.stack.drop(t.Attr().ID())
tracker.cost += common.SelectAndIdentCost
}
if !tracker.presenceTestHasCost {
if _, isTestOnly := programStep.(*evalTestOnly); isTestOnly {
tracker.cost -= common.SelectAndIdentCost
}
}
case *evalExhaustiveConditional:
// Ternary has no direct cost. All cost is from the conditional and the true/false branch expressions.
tracker.stack.drop(t.attr.falsy.ID(), t.attr.truthy.ID(), t.attr.expr.ID())
@ -95,21 +100,58 @@ func CostObserver(tracker *CostTracker) EvalObserver {
return observer
}
// CostTracker represents the information needed for tacking runtime cost
// CostTrackerOption configures the behavior of CostTracker objects.
type CostTrackerOption func(*CostTracker) error
// CostTrackerLimit sets the runtime limit on the evaluation cost during execution and will terminate the expression
// evaluation if the limit is exceeded.
func CostTrackerLimit(limit uint64) CostTrackerOption {
return func(tracker *CostTracker) error {
tracker.Limit = &limit
return nil
}
}
// PresenceTestHasCost determines whether presence testing has a cost of one or zero.
// Defaults to presence test has a cost of one.
func PresenceTestHasCost(hasCost bool) CostTrackerOption {
return func(tracker *CostTracker) error {
tracker.presenceTestHasCost = hasCost
return nil
}
}
// NewCostTracker creates a new CostTracker with a given estimator and a set of functional CostTrackerOption values.
func NewCostTracker(estimator ActualCostEstimator, opts ...CostTrackerOption) (*CostTracker, error) {
tracker := &CostTracker{
Estimator: estimator,
presenceTestHasCost: true,
}
for _, opt := range opts {
err := opt(tracker)
if err != nil {
return nil, err
}
}
return tracker, nil
}
// CostTracker represents the information needed for tracking runtime cost.
type CostTracker struct {
Estimator ActualCostEstimator
Limit *uint64
Estimator ActualCostEstimator
Limit *uint64
presenceTestHasCost bool
cost uint64
stack refValStack
}
// ActualCost returns the runtime cost
func (c CostTracker) ActualCost() uint64 {
func (c *CostTracker) ActualCost() uint64 {
return c.cost
}
func (c CostTracker) costCall(call InterpretableCall, argValues []ref.Val, result ref.Val) uint64 {
func (c *CostTracker) costCall(call InterpretableCall, argValues []ref.Val, result ref.Val) uint64 {
var cost uint64
if c.Estimator != nil {
callCost := c.Estimator.CallCost(call.Function(), call.OverloadID(), argValues, result)
@ -122,7 +164,7 @@ func (c CostTracker) costCall(call InterpretableCall, argValues []ref.Val, resul
// if user has their own implementation of ActualCostEstimator, make sure to cover the mapping between overloadId and cost calculation
switch call.OverloadID() {
// O(n) functions
case overloads.StartsWithString, overloads.EndsWithString, overloads.StringToBytes, overloads.BytesToString:
case overloads.StartsWithString, overloads.EndsWithString, overloads.StringToBytes, overloads.BytesToString, overloads.ExtQuoteString, overloads.ExtFormatString:
cost += uint64(math.Ceil(float64(c.actualSize(argValues[0])) * common.StringTraversalCostFactor))
case overloads.InList:
// If a list is composed entirely of constant values this is O(1), but we don't account for that here.
@ -179,7 +221,7 @@ func (c CostTracker) costCall(call InterpretableCall, argValues []ref.Val, resul
}
// actualSize returns the size of value
func (c CostTracker) actualSize(value ref.Val) uint64 {
func (c *CostTracker) actualSize(value ref.Val) uint64 {
if sz, ok := value.(traits.Sizer); ok {
return uint64(sz.Size().(types.Int))
}