build: move e2e dependencies into e2e/go.mod

Several packages are only used while running the e2e suite. These
packages are less important to update, as the they can not influence the
final executable that is part of the Ceph-CSI container-image.

By moving these dependencies out of the main Ceph-CSI go.mod, it is
easier to identify if a reported CVE affects Ceph-CSI, or only the
testing (like most of the Kubernetes CVEs).

Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
Niels de Vos
2025-03-04 08:57:28 +01:00
committed by mergify[bot]
parent 15da101b1b
commit bec6090996
8047 changed files with 1407827 additions and 3453 deletions

View File

@ -0,0 +1,74 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
package(
default_visibility = ["//visibility:public"],
licenses = ["notice"], # Apache 2.0
)
go_library(
name = "go_default_library",
srcs = [
"activation.go",
"attribute_patterns.go",
"attributes.go",
"decorators.go",
"dispatcher.go",
"evalstate.go",
"interpretable.go",
"interpreter.go",
"optimizations.go",
"planner.go",
"prune.go",
"runtimecost.go",
],
importpath = "github.com/google/cel-go/interpreter",
deps = [
"//common:go_default_library",
"//common/ast:go_default_library",
"//common/containers:go_default_library",
"//common/functions:go_default_library",
"//common/operators:go_default_library",
"//common/overloads:go_default_library",
"//common/types:go_default_library",
"//common/types/ref:go_default_library",
"//common/types/traits: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",
"@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
"@org_golang_google_protobuf//types/known/wrapperspb:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"activation_test.go",
"attribute_patterns_test.go",
"attributes_test.go",
"interpreter_test.go",
"prune_test.go",
"runtimecost_test.go",
],
embed = [
":go_default_library",
],
deps = [
"//checker:go_default_library",
"//common/containers:go_default_library",
"//common/debug:go_default_library",
"//common/decls:go_default_library",
"//common/functions:go_default_library",
"//common/operators:go_default_library",
"//common/stdlib:go_default_library",
"//common/types:go_default_library",
"//parser:go_default_library",
"//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_protobuf//proto:go_default_library",
"@org_golang_google_protobuf//types/known/anypb:go_default_library",
],
)

View File

@ -0,0 +1,168 @@
// Copyright 2018 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"
"github.com/google/cel-go/common/types/ref"
)
// Activation used to resolve identifiers by name and references by id.
//
// An Activation is the primary mechanism by which a caller supplies input into a CEL program.
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) (any, bool)
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
Parent() Activation
}
// EmptyActivation returns a variable-free activation.
func EmptyActivation() Activation {
return emptyActivation{}
}
// emptyActivation is a variable-free activation.
type emptyActivation struct{}
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]any`.
//
// Lazy bindings may be supplied within the map-based input in either of the following forms:
// - 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 types.Adapter configured in the environment.
func NewActivation(bindings any) (Activation, error) {
if bindings == nil {
return nil, errors.New("bindings must be non-nil")
}
a, isActivation := bindings.(Activation)
if isActivation {
return a, nil
}
m, isMap := bindings.(map[string]any)
if !isMap {
return nil, fmt.Errorf(
"activation input must be an activation or map[string]interface: got %T",
bindings)
}
return &mapActivation{bindings: m}, nil
}
// mapActivation which implements Activation and maps of named values.
//
// 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]any
}
// Parent implements the Activation interface method.
func (a *mapActivation) Parent() Activation {
return nil
}
// ResolveName implements the Activation interface method.
func (a *mapActivation) ResolveName(name string) (any, bool) {
obj, found := a.bindings[name]
if !found {
return nil, false
}
fn, isLazy := obj.(func() ref.Val)
if isLazy {
obj = fn()
a.bindings[name] = obj
}
fnRaw, isLazy := obj.(func() any)
if isLazy {
obj = fnRaw()
a.bindings[name] = obj
}
return obj, found
}
// hierarchicalActivation which implements Activation and contains a parent and
// child activation.
type hierarchicalActivation struct {
parent Activation
child Activation
}
// Parent implements the Activation interface method.
func (a *hierarchicalActivation) Parent() Activation {
return a.parent
}
// ResolveName implements the Activation interface method.
func (a *hierarchicalActivation) ResolveName(name string) (any, bool) {
if object, found := a.child.ResolveName(name); found {
return object, found
}
return a.parent.ResolveName(name)
}
// NewHierarchicalActivation takes two activations and produces a new one which prioritizes
// resolution in the child first and parent(s) second.
func NewHierarchicalActivation(parent Activation, child Activation) Activation {
return &hierarchicalActivation{parent, child}
}
// NewPartialActivation returns an Activation which contains a list of AttributePattern values
// 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]any.
func NewPartialActivation(bindings any,
unknowns ...*AttributePattern) (PartialActivation, error) {
a, err := NewActivation(bindings)
if err != nil {
return nil, err
}
return &partActivation{Activation: a, unknowns: unknowns}, nil
}
// PartialActivation extends the Activation interface with a set of UnknownAttributePatterns.
type PartialActivation interface {
Activation
// UnknownAttributePaths returns a set of AttributePattern values which match Attribute
// expressions for data accesses whose values are not yet known.
UnknownAttributePatterns() []*AttributePattern
}
// partActivation is the default implementations of the PartialActivation interface.
type partActivation struct {
Activation
unknowns []*AttributePattern
}
// UnknownAttributePatterns implements the PartialActivation interface method.
func (a *partActivation) UnknownAttributePatterns() []*AttributePattern {
return a.unknowns
}

View File

@ -0,0 +1,397 @@
// 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 (
"fmt"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// AttributePattern represents a top-level variable with an optional set of qualifier patterns.
//
// When using a CEL expression within a container, e.g. a package or namespace, the variable name
// in the pattern must match the qualified name produced during the variable namespace resolution.
// For example, if variable `c` appears in an expression whose container is `a.b`, the variable
// name supplied to the pattern must be `a.b.c`
//
// The qualifier patterns for attribute matching must be one of the following:
//
// - valid map key type: string, int, uint, bool
// - wildcard (*)
//
// Examples:
//
// 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
// a specific index `0` should match. And lastly, the third example matches any indexed access
// that later selects the 'name' field.
type AttributePattern struct {
variable string
qualifierPatterns []*AttributeQualifierPattern
}
// NewAttributePattern produces a new mutable AttributePattern based on a variable name.
func NewAttributePattern(variable string) *AttributePattern {
return &AttributePattern{
variable: variable,
qualifierPatterns: []*AttributeQualifierPattern{},
}
}
// QualString adds a string qualifier pattern to the AttributePattern. The string may be a valid
// identifier, or string map key including empty string.
func (apat *AttributePattern) QualString(pattern string) *AttributePattern {
apat.qualifierPatterns = append(apat.qualifierPatterns,
&AttributeQualifierPattern{value: pattern})
return apat
}
// QualInt adds an int qualifier pattern to the AttributePattern. The index may be either a map or
// list index.
func (apat *AttributePattern) QualInt(pattern int64) *AttributePattern {
apat.qualifierPatterns = append(apat.qualifierPatterns,
&AttributeQualifierPattern{value: pattern})
return apat
}
// QualUint adds an uint qualifier pattern for a map index operation to the AttributePattern.
func (apat *AttributePattern) QualUint(pattern uint64) *AttributePattern {
apat.qualifierPatterns = append(apat.qualifierPatterns,
&AttributeQualifierPattern{value: pattern})
return apat
}
// QualBool adds a bool qualifier pattern for a map index operation to the AttributePattern.
func (apat *AttributePattern) QualBool(pattern bool) *AttributePattern {
apat.qualifierPatterns = append(apat.qualifierPatterns,
&AttributeQualifierPattern{value: pattern})
return apat
}
// Wildcard adds a special sentinel qualifier pattern that will match any single qualifier.
func (apat *AttributePattern) Wildcard() *AttributePattern {
apat.qualifierPatterns = append(apat.qualifierPatterns,
&AttributeQualifierPattern{wildcard: true})
return apat
}
// VariableMatches returns true if the fully qualified variable matches the AttributePattern
// fully qualified variable name.
func (apat *AttributePattern) VariableMatches(variable string) bool {
return apat.variable == variable
}
// QualifierPatterns returns the set of AttributeQualifierPattern values on the AttributePattern.
func (apat *AttributePattern) QualifierPatterns() []*AttributeQualifierPattern {
return apat.qualifierPatterns
}
// AttributeQualifierPattern holds a wildcard or valued qualifier pattern.
type AttributeQualifierPattern struct {
wildcard bool
value any
}
// Matches returns true if the qualifier pattern is a wildcard, or the Qualifier implements the
// qualifierValueEquator interface and its IsValueEqualTo returns true for the qualifier pattern.
func (qpat *AttributeQualifierPattern) Matches(q Qualifier) bool {
if qpat.wildcard {
return true
}
qve, ok := q.(qualifierValueEquator)
return ok && qve.QualifierValueEquals(qpat.value)
}
// qualifierValueEquator defines an interface for determining if an input value, of valid map key
// type, is equal to the value held in the Qualifier. This interface is used by the
// AttributeQualifierPattern to determine pattern matches for non-wildcard qualifier patterns.
//
// Note: Attribute values are also Qualifier values; however, Attributes are resolved before
// qualification happens. This is an implementation detail, but one relevant to why the Attribute
// types do not surface in the list of implementations.
//
// See: partialAttributeFactory.matchesUnknownPatterns for more details on how this interface is
// used.
type qualifierValueEquator interface {
// QualifierValueEquals returns true if the input value is equal to the value held in the
// Qualifier.
QualifierValueEquals(value any) bool
}
// QualifierValueEquals implementation for boolean qualifiers.
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 any) bool {
sval, ok := value.(string)
return ok && q.Name == sval
}
// QualifierValueEquals implementation for string qualifiers.
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 any) bool {
return numericValueEquals(value, q.celValue)
}
// QualifierValueEquals implementation for uint qualifiers.
func (q *uintQualifier) QualifierValueEquals(value any) bool {
return numericValueEquals(value, q.celValue)
}
// QualifierValueEquals implementation for double qualifiers.
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 any, celValue ref.Val) bool {
val := types.DefaultTypeAdapter.NativeToValue(value)
return celValue.Equal(val) == types.True
}
// NewPartialAttributeFactory returns an AttributeFactory implementation capable of performing
// AttributePattern matches with PartialActivation inputs.
func NewPartialAttributeFactory(container *containers.Container, adapter types.Adapter, provider types.Provider, opts ...AttrFactoryOption) AttributeFactory {
fac := NewAttributeFactory(container, adapter, provider, opts...)
return &partialAttributeFactory{
AttributeFactory: fac,
container: container,
adapter: adapter,
provider: provider,
}
}
type partialAttributeFactory struct {
AttributeFactory
container *containers.Container
adapter types.Adapter
provider types.Provider
}
// AbsoluteAttribute implementation of the AttributeFactory interface which wraps the
// NamespacedAttribute resolution in an internal attributeMatcher object to dynamically match
// unknown patterns from PartialActivation inputs if given.
func (fac *partialAttributeFactory) AbsoluteAttribute(id int64, names ...string) NamespacedAttribute {
attr := fac.AttributeFactory.AbsoluteAttribute(id, names...)
return &attributeMatcher{fac: fac, NamespacedAttribute: attr}
}
// MaybeAttribute implementation of the AttributeFactory interface which ensure that the set of
// 'maybe' NamespacedAttribute values are produced using the partialAttributeFactory rather than
// the base AttributeFactory implementation.
func (fac *partialAttributeFactory) MaybeAttribute(id int64, name string) Attribute {
return &maybeAttribute{
id: id,
attrs: []NamespacedAttribute{
fac.AbsoluteAttribute(id, fac.container.ResolveCandidateNames(name)...),
},
adapter: fac.adapter,
provider: fac.provider,
fac: fac,
}
}
// matchesUnknownPatterns returns true if the variable names and qualifiers for a given
// Attribute value match any of the ActivationPattern objects in the set of unknown activation
// patterns on the given PartialActivation.
//
// For example, in the expression `a.b`, the Attribute is composed of variable `a`, with string
// qualifier `b`. When a PartialActivation is supplied, it indicates that some or all of the data
// provided in the input is unknown by specifying unknown AttributePatterns. An AttributePattern
// that refers to variable `a` with a string qualifier of `c` will not match `a.b`; however, any
// of the following patterns will match Attribute `a.b`:
//
// - `AttributePattern("a")`
// - `AttributePattern("a").Wildcard()`
// - `AttributePattern("a").QualString("b")`
// - `AttributePattern("a").QualString("b").QualInt(0)`
//
// Any AttributePattern which overlaps an Attribute or vice-versa will produce an Unknown result
// for the last pattern matched variable or qualifier in the Attribute. In the first matching
// example, the expression id representing variable `a` would be listed in the Unknown result,
// whereas in the other pattern examples, the qualifier `b` would be returned as the Unknown.
func (fac *partialAttributeFactory) matchesUnknownPatterns(
vars PartialActivation,
attrID int64,
variableNames []string,
qualifiers []Qualifier) (*types.Unknown, error) {
patterns := vars.UnknownAttributePatterns()
candidateIndices := map[int]struct{}{}
for _, variable := range variableNames {
for i, pat := range patterns {
if pat.VariableMatches(variable) {
if len(qualifiers) == 0 {
return types.NewUnknown(attrID, types.NewAttributeTrail(variable)), nil
}
candidateIndices[i] = struct{}{}
}
}
}
// Determine whether to return early if there are no candidate unknown patterns.
if len(candidateIndices) == 0 {
return nil, nil
}
// Resolve the attribute qualifiers into a static set. This prevents more dynamic
// Attribute resolutions than necessary when there are multiple unknown patterns
// that traverse the same Attribute-based qualifier field.
newQuals := make([]Qualifier, len(qualifiers))
for i, qual := range qualifiers {
attr, isAttr := qual.(Attribute)
if isAttr {
val, err := attr.Resolve(vars)
if err != nil {
return nil, err
}
// 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, attr.IsOptional())
if err != nil {
return nil, err
}
}
newQuals[i] = qual
}
// Determine whether any of the unknown patterns match.
for patIdx := range candidateIndices {
pat := patterns[patIdx]
isUnk := true
matchExprID := attrID
qualPats := pat.QualifierPatterns()
for i, qual := range newQuals {
if i >= len(qualPats) {
break
}
matchExprID = qual.ID()
qualPat := qualPats[i]
// Note, the AttributeQualifierPattern relies on the input Qualifier not being an
// Attribute, since there is no way to resolve the Attribute with the information
// provided to the Matches call.
if !qualPat.Matches(qual) {
isUnk = false
break
}
}
if isUnk {
attr := types.NewAttributeTrail(pat.variable)
for i := 0; i < len(qualPats) && i < len(newQuals); i++ {
if qual, ok := newQuals[i].(ConstantQualifier); ok {
switch v := qual.Value().Value().(type) {
case bool:
types.QualifyAttribute[bool](attr, v)
case float64:
types.QualifyAttribute[int64](attr, int64(v))
case int64:
types.QualifyAttribute[int64](attr, v)
case string:
types.QualifyAttribute[string](attr, v)
case uint64:
types.QualifyAttribute[uint64](attr, v)
default:
types.QualifyAttribute[string](attr, fmt.Sprintf("%v", v))
}
} else {
types.QualifyAttribute[string](attr, "*")
}
}
return types.NewUnknown(matchExprID, attr), nil
}
}
return nil, nil
}
// attributeMatcher embeds the NamespacedAttribute interface which allows it to participate in
// AttributePattern matching against Attribute values without having to modify the code paths that
// identify Attributes in expressions.
type attributeMatcher struct {
NamespacedAttribute
qualifiers []Qualifier
fac *partialAttributeFactory
}
// AddQualifier implements the Attribute interface method.
func (m *attributeMatcher) AddQualifier(qual Qualifier) (Attribute, error) {
// Add the qualifier to the embedded NamespacedAttribute. If the input to the Resolve
// method is not a PartialActivation, or does not match an unknown attribute pattern, the
// Resolve method is directly invoked on the underlying NamespacedAttribute.
_, err := m.NamespacedAttribute.AddQualifier(qual)
if err != nil {
return nil, err
}
// The attributeMatcher overloads TryResolve and will attempt to match unknown patterns against
// the variable name and qualifier set contained within the Attribute. These values are not
// directly inspectable on the top-level NamespacedAttribute interface and so are tracked within
// the attributeMatcher.
m.qualifiers = append(m.qualifiers, qual)
return m, nil
}
// 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) 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)
if isPartial {
unk, err := m.fac.matchesUnknownPatterns(
partial,
id,
m.CandidateVariableNames(),
m.qualifiers)
if err != nil {
return nil, err
}
if unk != nil {
return unk, nil
}
}
return m.NamespacedAttribute.Resolve(vars)
}
// Qualify is an implementation of the Qualifier interface method.
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) {
pv, ok := vars.(PartialActivation)
if ok {
return pv, true
}
if vars.Parent() != nil {
return toPartialActivation(vars.Parent())
}
return nil, false
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,272 @@
// Copyright 2018 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 (
"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"
)
// InterpretableDecorator is a functional interface for decorating or replacing
// Interpretable expression nodes at construction time.
type InterpretableDecorator func(Interpretable) (Interpretable, error)
// decObserveEval records evaluation state into an EvalState object.
func decObserveEval(observer EvalObserver) InterpretableDecorator {
return func(i Interpretable) (Interpretable, error) {
switch inst := i.(type) {
case *evalWatch, *evalWatchAttr, *evalWatchConst, *evalWatchConstructor:
// these instruction are already watching, return straight-away.
return i, nil
case InterpretableAttribute:
return &evalWatchAttr{
InterpretableAttribute: inst,
observer: observer,
}, nil
case InterpretableConst:
return &evalWatchConst{
InterpretableConst: inst,
observer: observer,
}, nil
case InterpretableConstructor:
return &evalWatchConstructor{
constructor: inst,
observer: observer,
}, nil
default:
return &evalWatch{
Interpretable: i,
observer: observer,
}, nil
}
}
}
// decInterruptFolds creates an intepretable decorator which marks comprehensions as interruptable
// where the interrupt state is communicated via a hidden variable on the Activation.
func decInterruptFolds() InterpretableDecorator {
return func(i Interpretable) (Interpretable, error) {
fold, ok := i.(*evalFold)
if !ok {
return i, nil
}
fold.interruptable = true
return fold, nil
}
}
// decDisableShortcircuits ensures that all branches of an expression will be evaluated, no short-circuiting.
func decDisableShortcircuits() InterpretableDecorator {
return func(i Interpretable) (Interpretable, error) {
switch expr := i.(type) {
case *evalOr:
return &evalExhaustiveOr{
id: expr.id,
terms: expr.terms,
}, nil
case *evalAnd:
return &evalExhaustiveAnd{
id: expr.id,
terms: expr.terms,
}, nil
case *evalFold:
expr.exhaustive = true
return expr, nil
case InterpretableAttribute:
cond, isCond := expr.Attr().(*conditionalAttribute)
if isCond {
return &evalExhaustiveConditional{
id: cond.id,
attr: cond,
adapter: expr.Adapter(),
}, nil
}
}
return i, nil
}
}
// decOptimize optimizes the program plan by looking for common evaluation patterns and
// conditionally precomputing the result.
// - build list and map values with constant elements.
// - convert 'in' operations to set membership tests if possible.
func decOptimize() InterpretableDecorator {
return func(i Interpretable) (Interpretable, error) {
switch inst := i.(type) {
case *evalList:
return maybeBuildListLiteral(i, inst)
case *evalMap:
return maybeBuildMapLiteral(i, inst)
case InterpretableCall:
if inst.OverloadID() == overloads.InList {
return maybeOptimizeSetMembership(i, inst)
}
if overloads.IsTypeConversionFunction(inst.Function()) {
return maybeOptimizeConstUnary(i, inst)
}
}
return i, nil
}
}
// decRegexOptimizer compiles regex pattern string constants.
func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDecorator {
functionMatchMap := make(map[string]*RegexOptimization)
overloadMatchMap := make(map[string]*RegexOptimization)
for _, m := range regexOptimizations {
functionMatchMap[m.Function] = m
if m.OverloadID != "" {
overloadMatchMap[m.OverloadID] = m
}
}
return func(i Interpretable) (Interpretable, error) {
call, ok := i.(InterpretableCall)
if !ok {
return i, nil
}
var matcher *RegexOptimization
var found bool
if call.OverloadID() != "" {
matcher, found = overloadMatchMap[call.OverloadID()]
}
if !found {
matcher, found = functionMatchMap[call.Function()]
}
if !found || matcher.RegexIndex >= len(call.Args()) {
return i, nil
}
args := call.Args()
regexArg := args[matcher.RegexIndex]
regexStr, isConst := regexArg.(InterpretableConst)
if !isConst {
return i, nil
}
pattern, ok := regexStr.Value().(types.String)
if !ok {
return i, nil
}
return matcher.Factory(call, string(pattern))
}
}
func maybeOptimizeConstUnary(i Interpretable, call InterpretableCall) (Interpretable, error) {
args := call.Args()
if len(args) != 1 {
return i, nil
}
_, isConst := args[0].(InterpretableConst)
if !isConst {
return i, nil
}
val := call.Eval(EmptyActivation())
if types.IsError(val) {
return nil, val.(*types.Err)
}
return NewConstValue(call.ID(), val), nil
}
func maybeBuildListLiteral(i Interpretable, l *evalList) (Interpretable, error) {
for _, elem := range l.elems {
_, isConst := elem.(InterpretableConst)
if !isConst {
return i, nil
}
}
return NewConstValue(l.ID(), l.Eval(EmptyActivation())), nil
}
func maybeBuildMapLiteral(i Interpretable, mp *evalMap) (Interpretable, error) {
for idx, key := range mp.keys {
_, isConst := key.(InterpretableConst)
if !isConst {
return i, nil
}
_, isConst = mp.vals[idx].(InterpretableConst)
if !isConst {
return i, nil
}
}
return NewConstValue(mp.ID(), mp.Eval(EmptyActivation())), nil
}
// maybeOptimizeSetMembership may convert an 'in' operation against a list to map key membership
// test if the following conditions are true:
// - the list is a constant with homogeneous element types.
// - the elements are all of primitive type.
func maybeOptimizeSetMembership(i Interpretable, inlist InterpretableCall) (Interpretable, error) {
args := inlist.Args()
lhs := args[0]
rhs := args[1]
l, isConst := rhs.(InterpretableConst)
if !isConst {
return i, nil
}
// When the incoming binary call is flagged with as the InList overload, the value will
// always be convertible to a `traits.Lister` type.
list := l.Value().(traits.Lister)
if list.Size() == types.IntZero {
return NewConstValue(inlist.ID(), types.False), nil
}
it := list.Iterator()
valueSet := make(map[ref.Val]ref.Val)
for it.HasNext() == types.True {
elem := it.Next()
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
switch ev := elem.(type) {
case types.Double:
iv := ev.ConvertToType(types.IntType)
// Ensure that only lossless conversions are added to the set
if !types.IsError(iv) && iv.Equal(ev) == types.True {
valueSet[iv] = types.True
}
// Ensure that only lossless conversions are added to the set
uv := ev.ConvertToType(types.UintType)
if !types.IsError(uv) && uv.Equal(ev) == types.True {
valueSet[uv] = types.True
}
case types.Int:
dv := ev.ConvertToType(types.DoubleType)
if !types.IsError(dv) {
valueSet[dv] = types.True
}
uv := ev.ConvertToType(types.UintType)
if !types.IsError(uv) {
valueSet[uv] = types.True
}
case types.Uint:
dv := ev.ConvertToType(types.DoubleType)
if !types.IsError(dv) {
valueSet[dv] = types.True
}
iv := ev.ConvertToType(types.IntType)
if !types.IsError(iv) {
valueSet[iv] = types.True
}
}
}
return &evalSetMembership{
inst: inlist,
arg: lhs,
valueSet: valueSet,
}, nil
}

View File

@ -0,0 +1,100 @@
// Copyright 2018 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 (
"fmt"
"github.com/google/cel-go/common/functions"
)
// Dispatcher resolves function calls to their appropriate overload.
type Dispatcher interface {
// Add one or more overloads, returning an error if any Overload has the same Overload#Name.
Add(overloads ...*functions.Overload) error
// FindOverload returns an Overload definition matching the provided name.
FindOverload(overload string) (*functions.Overload, bool)
// OverloadIds returns the set of all overload identifiers configured for dispatch.
OverloadIds() []string
}
// NewDispatcher returns an empty Dispatcher instance.
func NewDispatcher() Dispatcher {
return &defaultDispatcher{
overloads: make(map[string]*functions.Overload)}
}
// ExtendDispatcher returns a Dispatcher which inherits the overloads of its parent, and
// provides an isolation layer between built-ins and extension functions which is useful
// for forward compatibility.
func ExtendDispatcher(parent Dispatcher) Dispatcher {
return &defaultDispatcher{
parent: parent,
overloads: make(map[string]*functions.Overload)}
}
// overloadMap helper type for indexing overloads by function name.
type overloadMap map[string]*functions.Overload
// defaultDispatcher struct which contains an overload map.
type defaultDispatcher struct {
parent Dispatcher
overloads overloadMap
}
// Add implements the Dispatcher.Add interface method.
func (d *defaultDispatcher) Add(overloads ...*functions.Overload) error {
for _, o := range overloads {
// add the overload unless an overload of the same name has already been provided.
if _, found := d.overloads[o.Operator]; found {
return fmt.Errorf("overload already exists '%s'", o.Operator)
}
// index the overload by function name.
d.overloads[o.Operator] = o
}
return nil
}
// FindOverload implements the Dispatcher.FindOverload interface method.
func (d *defaultDispatcher) FindOverload(overload string) (*functions.Overload, bool) {
o, found := d.overloads[overload]
// Attempt to dispatch to an overload defined in the parent.
if !found && d.parent != nil {
return d.parent.FindOverload(overload)
}
return o, found
}
// OverloadIds implements the Dispatcher interface method.
func (d *defaultDispatcher) OverloadIds() []string {
i := 0
overloads := make([]string, len(d.overloads))
for name := range d.overloads {
overloads[i] = name
i++
}
if d.parent == nil {
return overloads
}
parentOverloads := d.parent.OverloadIds()
for _, pName := range parentOverloads {
if _, found := d.overloads[pName]; !found {
overloads = append(overloads, pName)
}
}
return overloads
}

View File

@ -0,0 +1,79 @@
// Copyright 2018 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 (
"github.com/google/cel-go/common/types/ref"
)
// EvalState tracks the values associated with expression ids during execution.
type EvalState interface {
// IDs returns the list of ids with recorded values.
IDs() []int64
// Value returns the observed value of the given expression id if found, and a nil false
// result if not.
Value(int64) (ref.Val, bool)
// SetValue sets the observed value of the expression id.
SetValue(int64, ref.Val)
// Reset clears the previously recorded expression values.
Reset()
}
// evalState permits the mutation of evaluation state for a given expression id.
type evalState struct {
values map[int64]ref.Val
}
// NewEvalState returns an EvalState instanced used to observe the intermediate
// evaluations of an expression.
func NewEvalState() EvalState {
return &evalState{
values: make(map[int64]ref.Val),
}
}
// IDs implements the EvalState interface method.
func (s *evalState) IDs() []int64 {
var ids []int64
for k, v := range s.values {
if v != nil {
ids = append(ids, k)
}
}
return ids
}
// Value is an implementation of the EvalState interface method.
func (s *evalState) Value(exprID int64) (ref.Val, bool) {
val, found := s.values[exprID]
return val, found
}
// SetValue is an implementation of the EvalState interface method.
func (s *evalState) SetValue(exprID int64, val ref.Val) {
if val == nil {
delete(s.values, exprID)
} else {
s.values[exprID] = val
}
}
// Reset implements the EvalState interface method.
func (s *evalState) Reset() {
s.values = map[int64]ref.Val{}
}

View File

@ -0,0 +1,17 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
package(
default_visibility = ["//visibility:public"],
licenses = ["notice"], # Apache 2.0
)
go_library(
name = "go_default_library",
srcs = [
"functions.go",
],
importpath = "github.com/google/cel-go/interpreter/functions",
deps = [
"//common/functions:go_default_library",
],
)

View File

@ -0,0 +1,39 @@
// Copyright 2018 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 functions defines the standard builtin functions supported by the
// interpreter and as declared within the checker#StandardDeclarations.
package functions
import fn "github.com/google/cel-go/common/functions"
// Overload defines a named overload of a function, indicating an operand trait
// which must be present on the first argument to the overload as well as one
// of either a unary, binary, or function implementation.
//
// The majority of operators within the expression language are unary or binary
// and the specializations simplify the call contract for implementers of
// types with operator overloads. Any added complexity is assumed to be handled
// by the generic FunctionOp.
type Overload = fn.Overload
// UnaryOp is a function that takes a single value and produces an output.
type UnaryOp = fn.UnaryOp
// BinaryOp is a function that takes two values and produces an output.
type BinaryOp = fn.BinaryOp
// FunctionOp is a function with accepts zero or more arguments and produces
// a value or error as a result.
type FunctionOp = fn.FunctionOp

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
// Copyright 2018 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 provides functions to evaluate parsed expressions with
// the option to augment the evaluation with inputs and functions supplied at
// evaluation time.
package interpreter
import (
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// Interpreter generates a new Interpretable from a checked or unchecked expression.
type Interpreter interface {
// NewInterpretable creates an Interpretable from a checked expression and an
// optional list of InterpretableDecorator values.
NewInterpretable(exprAST *ast.AST, 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 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.
func Observe(observers ...EvalObserver) InterpretableDecorator {
if len(observers) == 1 {
return decObserveEval(observers[0])
}
observeFn := func(id int64, programStep any, val ref.Val) {
for _, observer := range observers {
observer(id, programStep, val)
}
}
return decObserveEval(observeFn)
}
// EvalCancelledError represents a cancelled program evaluation operation.
type EvalCancelledError struct {
Message string
// Type identifies the cause of the cancellation.
Cause CancellationCause
}
func (e EvalCancelledError) Error() string {
return e.Message
}
// CancellationCause enumerates the ways a program evaluation operation can be cancelled.
type CancellationCause int
const (
// ContextCancelled indicates that the operation was cancelled in response to a Golang context cancellation.
ContextCancelled CancellationCause = iota
// CostLimitExceeded indicates that the operation was cancelled in response to the actual cost limit being
// exceeded.
CostLimitExceeded
)
// TODO: Replace all usages of TrackState with EvalStateObserver
// TrackState decorates each expression node with an observer which records the value
// associated with the given expression id. EvalState must be provided to the decorator.
// This decorator is not thread-safe, and the EvalState must be reset between Eval()
// calls.
// DEPRECATED: Please use EvalStateObserver instead. It composes gracefully with additional observers.
func TrackState(state EvalState) InterpretableDecorator {
return Observe(EvalStateObserver(state))
}
// EvalStateObserver provides an observer which records the value
// associated with the given expression id. EvalState must be provided to the observer.
// 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 any, val ref.Val) {
state.SetValue(id, val)
}
}
// ExhaustiveEval replaces operations that short-circuit with versions that evaluate
// expressions and couples this behavior with the TrackState() decorator to provide
// insight into the evaluation state of the entire expression. EvalState must be
// provided to the decorator. This decorator is not thread-safe, and the EvalState
// must be reset between Eval() calls.
func ExhaustiveEval() InterpretableDecorator {
ex := decDisableShortcircuits()
return func(i Interpretable) (Interpretable, error) {
return ex(i)
}
}
// InterruptableEval annotates comprehension loops with information that indicates they
// should check the `#interrupted` state within a custom Activation.
//
// The custom activation is currently managed higher up in the stack within the 'cel' package
// and should not require any custom support on behalf of callers.
func InterruptableEval() InterpretableDecorator {
return decInterruptFolds()
}
// Optimize will pre-compute operations such as list and map construction and optimize
// call arguments to set membership tests. The set of optimizations will increase over time.
func Optimize() InterpretableDecorator {
return decOptimize()
}
// RegexOptimization provides a way to replace an InterpretableCall for a regex function when the
// RegexIndex argument is a string constant. Typically, the Factory would compile the regex pattern at
// RegexIndex and report any errors (at program creation time) and then use the compiled regex for
// all regex function invocations.
type RegexOptimization struct {
// Function is the name of the function to optimize.
Function string
// OverloadID is the ID of the overload to optimize.
OverloadID string
// RegexIndex is the index position of the regex pattern argument. Only calls to the function where this argument is
// a string constant will be delegated to this optimizer.
RegexIndex int
// Factory constructs a replacement InterpretableCall node that optimizes the regex function call. Factory is
// provided with the unoptimized regex call and the string constant at the RegexIndex argument.
// The Factory may compile the regex for use across all invocations of the call, return any errors and
// return an interpreter.NewCall with the desired regex optimized function impl.
Factory func(call InterpretableCall, regexPattern string) (InterpretableCall, error)
}
// CompileRegexConstants compiles regex pattern string constants at program creation time and reports any regex pattern
// compile errors.
func CompileRegexConstants(regexOptimizations ...*RegexOptimization) InterpretableDecorator {
return decRegexOptimizer(regexOptimizations...)
}
type exprInterpreter struct {
dispatcher Dispatcher
container *containers.Container
provider types.Provider
adapter types.Adapter
attrFactory AttributeFactory
}
// NewInterpreter builds an Interpreter from a Dispatcher and TypeProvider which will be used
// throughout the Eval of all Interpretable instances generated from it.
func NewInterpreter(dispatcher Dispatcher,
container *containers.Container,
provider types.Provider,
adapter types.Adapter,
attrFactory AttributeFactory) Interpreter {
return &exprInterpreter{
dispatcher: dispatcher,
container: container,
provider: provider,
adapter: adapter,
attrFactory: attrFactory}
}
// NewIntepretable implements the Interpreter interface method.
func (i *exprInterpreter) NewInterpretable(
checked *ast.AST,
decorators ...InterpretableDecorator) (Interpretable, error) {
p := newPlanner(
i.dispatcher,
i.provider,
i.adapter,
i.attrFactory,
i.container,
checked,
decorators...)
return p.Plan(checked.Expr())
}

View File

@ -0,0 +1,46 @@
// Copyright 2022 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 (
"regexp"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// MatchesRegexOptimization optimizes the 'matches' standard library function by compiling the regex pattern and
// reporting any compilation errors at program creation time, and using the compiled regex pattern for all function
// call invocations.
var MatchesRegexOptimization = &RegexOptimization{
Function: "matches",
RegexIndex: 1,
Factory: func(call InterpretableCall, regexPattern string) (InterpretableCall, error) {
compiledRegex, err := regexp.Compile(regexPattern)
if err != nil {
return nil, err
}
return NewCall(call.ID(), call.Function(), call.OverloadID(), call.Args(), func(values ...ref.Val) ref.Val {
if len(values) != 2 {
return types.NoSuchOverloadErr()
}
in, ok := values[0].Value().(string)
if !ok {
return types.NoSuchOverloadErr()
}
return types.Bool(compiledRegex.MatchString(in))
}), nil
},
}

View File

@ -0,0 +1,757 @@
// Copyright 2018 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 (
"fmt"
"strings"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/functions"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types"
)
// interpretablePlanner creates an Interpretable evaluation plan from a proto Expr value.
type interpretablePlanner interface {
// Plan generates an Interpretable value (or error) from the input proto Expr.
Plan(expr ast.Expr) (Interpretable, error)
}
// newPlanner creates an interpretablePlanner which references a Dispatcher, TypeProvider,
// TypeAdapter, Container, and CheckedExpr value. These pieces of data are used to resolve
// functions, types, and namespaced identifiers at plan time rather than at runtime since
// it only needs to be done once and may be semi-expensive to compute.
func newPlanner(disp Dispatcher,
provider types.Provider,
adapter types.Adapter,
attrFactory AttributeFactory,
cont *containers.Container,
exprAST *ast.AST,
decorators ...InterpretableDecorator) interpretablePlanner {
return &planner{
disp: disp,
provider: provider,
adapter: adapter,
attrFactory: attrFactory,
container: cont,
refMap: exprAST.ReferenceMap(),
typeMap: exprAST.TypeMap(),
decorators: decorators,
}
}
// planner is an implementation of the interpretablePlanner interface.
type planner struct {
disp Dispatcher
provider types.Provider
adapter types.Adapter
attrFactory AttributeFactory
container *containers.Container
refMap map[int64]*ast.ReferenceInfo
typeMap map[int64]*types.Type
decorators []InterpretableDecorator
}
// Plan implements the interpretablePlanner interface. This implementation of the Plan method also
// applies decorators to each Interpretable generated as part of the overall plan. Decorators are
// useful for layering functionality into the evaluation that is not natively understood by CEL,
// such as state-tracking, expression re-write, and possibly efficient thread-safe memoization of
// repeated expressions.
func (p *planner) Plan(expr ast.Expr) (Interpretable, error) {
switch expr.Kind() {
case ast.CallKind:
return p.decorate(p.planCall(expr))
case ast.IdentKind:
return p.decorate(p.planIdent(expr))
case ast.LiteralKind:
return p.decorate(p.planConst(expr))
case ast.SelectKind:
return p.decorate(p.planSelect(expr))
case ast.ListKind:
return p.decorate(p.planCreateList(expr))
case ast.MapKind:
return p.decorate(p.planCreateMap(expr))
case ast.StructKind:
return p.decorate(p.planCreateStruct(expr))
case ast.ComprehensionKind:
return p.decorate(p.planComprehension(expr))
}
return nil, fmt.Errorf("unsupported expr: %v", expr)
}
// decorate applies the InterpretableDecorator functions to the given Interpretable.
// Both the Interpretable and error generated by a Plan step are accepted as arguments
// for convenience.
func (p *planner) decorate(i Interpretable, err error) (Interpretable, error) {
if err != nil {
return nil, err
}
for _, dec := range p.decorators {
i, err = dec(i)
if err != nil {
return nil, err
}
}
return i, nil
}
// planIdent creates an Interpretable that resolves an identifier from an Activation.
func (p *planner) planIdent(expr ast.Expr) (Interpretable, error) {
// Establish whether the identifier is in the reference map.
if identRef, found := p.refMap[expr.ID()]; found {
return p.planCheckedIdent(expr.ID(), identRef)
}
// Create the possible attribute list for the unresolved reference.
ident := expr.AsIdent()
return &evalAttr{
adapter: p.adapter,
attr: p.attrFactory.MaybeAttribute(expr.ID(), ident),
}, nil
}
func (p *planner) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (Interpretable, error) {
// Plan a constant reference if this is the case for this simple identifier.
if identRef.Value != nil {
return NewConstValue(id, identRef.Value), nil
}
// Check to see whether the type map indicates this is a type name. All types should be
// registered with the provider.
cType := p.typeMap[id]
if cType.Kind() == types.TypeKind {
cVal, found := p.provider.FindIdent(identRef.Name)
if !found {
return nil, fmt.Errorf("reference to undefined type: %s", identRef.Name)
}
return NewConstValue(id, cVal), nil
}
// Otherwise, return the attribute for the resolved identifier name.
return &evalAttr{
adapter: p.adapter,
attr: p.attrFactory.AbsoluteAttribute(id, identRef.Name),
}, nil
}
// planSelect creates an Interpretable with either:
//
// a) selects a field from a map or proto.
// b) creates a field presence test for a select within a has() macro.
// c) resolves the select expression to a namespaced identifier.
func (p *planner) planSelect(expr ast.Expr) (Interpretable, error) {
// If the Select id appears in the reference map from the CheckedExpr proto then it is either
// a namespaced identifier or enum value.
if identRef, found := p.refMap[expr.ID()]; found {
return p.planCheckedIdent(expr.ID(), identRef)
}
sel := expr.AsSelect()
// Plan the operand evaluation.
op, err := p.Plan(sel.Operand())
if err != nil {
return nil, err
}
opType := p.typeMap[sel.Operand().ID()]
// If the Select was marked TestOnly, this is a presence test.
//
// Note: presence tests are defined for structured (e.g. proto) and dynamic values (map, json)
// as follows:
// - True if the object field has a non-default value, e.g. obj.str != ""
// - True if the dynamic value has the field defined, e.g. key in map
//
// However, presence tests are not defined for qualified identifier names with primitive types.
// 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.
// Establish the attribute reference.
attr, isAttr := op.(InterpretableAttribute)
if !isAttr {
attr, err = p.relativeAttr(op.ID(), op, false)
if err != nil {
return nil, err
}
}
// Build a qualifier for the attribute.
qual, err := p.attrFactory.NewQualifier(opType, expr.ID(), sel.FieldName(), false)
if err != nil {
return nil, err
}
// Modify the attribute to be test-only.
if sel.IsTestOnly() {
attr = &evalTestOnly{
id: expr.ID(),
InterpretableAttribute: attr,
}
}
// 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
// patterns. Specifically, conditional operators &&, ||, ?:, and (in)equality functions result in
// optimized Interpretable values.
func (p *planner) planCall(expr ast.Expr) (Interpretable, error) {
call := expr.AsCall()
target, fnName, oName := p.resolveFunction(expr)
argCount := len(call.Args())
var offset int
if target != nil {
argCount++
offset++
}
args := make([]Interpretable, argCount)
if target != nil {
arg, err := p.Plan(target)
if err != nil {
return nil, err
}
args[0] = arg
}
for i, argExpr := range call.Args() {
arg, err := p.Plan(argExpr)
if err != nil {
return nil, err
}
args[i+offset] = arg
}
// Generate specialized Interpretable operators by function name if possible.
switch fnName {
case operators.LogicalAnd:
return p.planCallLogicalAnd(expr, args)
case operators.LogicalOr:
return p.planCallLogicalOr(expr, args)
case operators.Conditional:
return p.planCallConditional(expr, args)
case operators.Equals:
return p.planCallEqual(expr, args)
case operators.NotEquals:
return p.planCallNotEqual(expr, args)
case operators.Index:
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.
// Try to find the specific function by overload id.
var fnDef *functions.Overload
if oName != "" {
fnDef, _ = p.disp.FindOverload(oName)
}
// If the overload id couldn't resolve the function, try the simple function name.
if fnDef == nil {
fnDef, _ = p.disp.FindOverload(fnName)
}
switch argCount {
case 0:
return p.planCallZero(expr, fnName, oName, fnDef)
case 1:
// If the FunctionOp has been used, then use it as it may exist for the purposes
// of dynamic dispatch within a singleton function implementation.
if fnDef != nil && fnDef.Unary == nil && fnDef.Function != nil {
return p.planCallVarArgs(expr, fnName, oName, fnDef, args)
}
return p.planCallUnary(expr, fnName, oName, fnDef, args)
case 2:
// If the FunctionOp has been used, then use it as it may exist for the purposes
// of dynamic dispatch within a singleton function implementation.
if fnDef != nil && fnDef.Binary == nil && fnDef.Function != nil {
return p.planCallVarArgs(expr, fnName, oName, fnDef, args)
}
return p.planCallBinary(expr, fnName, oName, fnDef, args)
default:
return p.planCallVarArgs(expr, fnName, oName, fnDef, args)
}
}
// planCallZero generates a zero-arity callable Interpretable.
func (p *planner) planCallZero(expr ast.Expr,
function string,
overload string,
impl *functions.Overload) (Interpretable, error) {
if impl == nil || impl.Function == nil {
return nil, fmt.Errorf("no such overload: %s()", function)
}
return &evalZeroArity{
id: expr.ID(),
function: function,
overload: overload,
impl: impl.Function,
}, nil
}
// planCallUnary generates a unary callable Interpretable.
func (p *planner) planCallUnary(expr ast.Expr,
function string,
overload string,
impl *functions.Overload,
args []Interpretable) (Interpretable, error) {
var fn functions.UnaryOp
var trait int
var nonStrict bool
if impl != nil {
if impl.Unary == nil {
return nil, fmt.Errorf("no such overload: %s(arg)", function)
}
fn = impl.Unary
trait = impl.OperandTrait
nonStrict = impl.NonStrict
}
return &evalUnary{
id: expr.ID(),
function: function,
overload: overload,
arg: args[0],
trait: trait,
impl: fn,
nonStrict: nonStrict,
}, nil
}
// planCallBinary generates a binary callable Interpretable.
func (p *planner) planCallBinary(expr ast.Expr,
function string,
overload string,
impl *functions.Overload,
args []Interpretable) (Interpretable, error) {
var fn functions.BinaryOp
var trait int
var nonStrict bool
if impl != nil {
if impl.Binary == nil {
return nil, fmt.Errorf("no such overload: %s(lhs, rhs)", function)
}
fn = impl.Binary
trait = impl.OperandTrait
nonStrict = impl.NonStrict
}
return &evalBinary{
id: expr.ID(),
function: function,
overload: overload,
lhs: args[0],
rhs: args[1],
trait: trait,
impl: fn,
nonStrict: nonStrict,
}, nil
}
// planCallVarArgs generates a variable argument callable Interpretable.
func (p *planner) planCallVarArgs(expr ast.Expr,
function string,
overload string,
impl *functions.Overload,
args []Interpretable) (Interpretable, error) {
var fn functions.FunctionOp
var trait int
var nonStrict bool
if impl != nil {
if impl.Function == nil {
return nil, fmt.Errorf("no such overload: %s(...)", function)
}
fn = impl.Function
trait = impl.OperandTrait
nonStrict = impl.NonStrict
}
return &evalVarArgs{
id: expr.ID(),
function: function,
overload: overload,
args: args,
trait: trait,
impl: fn,
nonStrict: nonStrict,
}, nil
}
// planCallEqual generates an equals (==) Interpretable.
func (p *planner) planCallEqual(expr ast.Expr, args []Interpretable) (Interpretable, error) {
return &evalEq{
id: expr.ID(),
lhs: args[0],
rhs: args[1],
}, nil
}
// planCallNotEqual generates a not equals (!=) Interpretable.
func (p *planner) planCallNotEqual(expr ast.Expr, args []Interpretable) (Interpretable, error) {
return &evalNe{
id: expr.ID(),
lhs: args[0],
rhs: args[1],
}, nil
}
// planCallLogicalAnd generates a logical and (&&) Interpretable.
func (p *planner) planCallLogicalAnd(expr ast.Expr, args []Interpretable) (Interpretable, error) {
return &evalAnd{
id: expr.ID(),
terms: args,
}, nil
}
// planCallLogicalOr generates a logical or (||) Interpretable.
func (p *planner) planCallLogicalOr(expr ast.Expr, args []Interpretable) (Interpretable, error) {
return &evalOr{
id: expr.ID(),
terms: args,
}, nil
}
// planCallConditional generates a conditional / ternary (c ? t : f) Interpretable.
func (p *planner) planCallConditional(expr ast.Expr, args []Interpretable) (Interpretable, error) {
cond := args[0]
t := args[1]
var tAttr Attribute
truthyAttr, isTruthyAttr := t.(InterpretableAttribute)
if isTruthyAttr {
tAttr = truthyAttr.Attr()
} else {
tAttr = p.attrFactory.RelativeAttribute(t.ID(), t)
}
f := args[2]
var fAttr Attribute
falsyAttr, isFalsyAttr := f.(InterpretableAttribute)
if isFalsyAttr {
fAttr = falsyAttr.Attr()
} else {
fAttr = p.attrFactory.RelativeAttribute(f.ID(), f)
}
return &evalAttr{
adapter: p.adapter,
attr: p.attrFactory.ConditionalAttribute(expr.ID(), cond, tAttr, fAttr),
}, nil
}
// 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 ast.Expr, args []Interpretable, optional bool) (Interpretable, error) {
op := args[0]
ind := args[1]
opType := p.typeMap[op.ID()]
// 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
}
}
// Construct the qualifier type.
var qual Qualifier
switch ind := ind.(type) {
case InterpretableConst:
qual, err = p.attrFactory.NewQualifier(opType, expr.ID(), ind.Value(), optional)
case InterpretableAttribute:
qual, err = p.attrFactory.NewQualifier(opType, expr.ID(), ind, optional)
default:
qual, err = p.relativeAttr(expr.ID(), ind, optional)
}
if err != nil {
return nil, 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 ast.Expr) (Interpretable, error) {
list := expr.AsList()
optionalIndices := list.OptionalIndices()
elements := list.Elements()
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
}
elems[i] = elemVal
}
return &evalList{
id: expr.ID(),
elems: elems,
optionals: optionals,
hasOptionals: len(optionals) != 0,
adapter: p.adapter,
}, nil
}
// planCreateStruct generates a map or object construction Interpretable.
func (p *planner) planCreateMap(expr ast.Expr) (Interpretable, error) {
m := expr.AsMap()
entries := m.Entries()
optionals := make([]bool, len(entries))
keys := make([]Interpretable, len(entries))
vals := make([]Interpretable, len(entries))
for i, e := range entries {
entry := e.AsMapEntry()
keyVal, err := p.Plan(entry.Key())
if err != nil {
return nil, err
}
keys[i] = keyVal
valVal, err := p.Plan(entry.Value())
if err != nil {
return nil, err
}
vals[i] = valVal
optionals[i] = entry.IsOptional()
}
return &evalMap{
id: expr.ID(),
keys: keys,
vals: vals,
optionals: optionals,
hasOptionals: len(optionals) != 0,
adapter: p.adapter,
}, nil
}
// planCreateObj generates an object construction Interpretable.
func (p *planner) planCreateStruct(expr ast.Expr) (Interpretable, error) {
obj := expr.AsStruct()
typeName, defined := p.resolveTypeName(obj.TypeName())
if !defined {
return nil, fmt.Errorf("unknown type: %s", obj.TypeName())
}
objFields := obj.Fields()
optionals := make([]bool, len(objFields))
fields := make([]string, len(objFields))
vals := make([]Interpretable, len(objFields))
for i, f := range objFields {
field := f.AsStructField()
fields[i] = field.Name()
val, err := p.Plan(field.Value())
if err != nil {
return nil, err
}
vals[i] = val
optionals[i] = field.IsOptional()
}
return &evalObj{
id: expr.ID(),
typeName: typeName,
fields: fields,
vals: vals,
optionals: optionals,
hasOptionals: len(optionals) != 0,
provider: p.provider,
}, nil
}
// planComprehension generates an Interpretable fold operation.
func (p *planner) planComprehension(expr ast.Expr) (Interpretable, error) {
fold := expr.AsComprehension()
accu, err := p.Plan(fold.AccuInit())
if err != nil {
return nil, err
}
iterRange, err := p.Plan(fold.IterRange())
if err != nil {
return nil, err
}
cond, err := p.Plan(fold.LoopCondition())
if err != nil {
return nil, err
}
step, err := p.Plan(fold.LoopStep())
if err != nil {
return nil, err
}
result, err := p.Plan(fold.Result())
if err != nil {
return nil, err
}
return &evalFold{
id: expr.ID(),
accuVar: fold.AccuVar(),
accu: accu,
iterVar: fold.IterVar(),
iterVar2: fold.IterVar2(),
iterRange: iterRange,
cond: cond,
step: step,
result: result,
adapter: p.adapter,
}, nil
}
// planConst generates a constant valued Interpretable.
func (p *planner) planConst(expr ast.Expr) (Interpretable, error) {
return NewConstValue(expr.ID(), expr.AsLiteral()), nil
}
// resolveTypeName takes a qualified string constructed at parse time, applies the proto
// namespace resolution rules to it in a scan over possible matching types in the TypeProvider.
func (p *planner) resolveTypeName(typeName string) (string, bool) {
for _, qualifiedTypeName := range p.container.ResolveCandidateNames(typeName) {
if _, found := p.provider.FindStructType(qualifiedTypeName); found {
return qualifiedTypeName, true
}
}
return "", false
}
// resolveFunction determines the call target, function name, and overload name from a given Expr
// value.
//
// The resolveFunction resolves ambiguities where a function may either be a receiver-style
// invocation or a qualified global function name.
// - The target expression may only consist of ident and select expressions.
// - The function is declared in the environment using its fully-qualified name.
// - The fully-qualified function name matches the string serialized target value.
func (p *planner) resolveFunction(expr ast.Expr) (ast.Expr, string, string) {
// Note: similar logic exists within the `checker/checker.go`. If making changes here
// please consider the impact on checker.go and consolidate implementations or mirror code
// as appropriate.
call := expr.AsCall()
var target ast.Expr = nil
if call.IsMemberFunction() {
target = call.Target()
}
fnName := call.FunctionName()
// Checked expressions always have a reference map entry, and _should_ have the fully qualified
// function name as the fnName value.
oRef, hasOverload := p.refMap[expr.ID()]
if hasOverload {
if len(oRef.OverloadIDs) == 1 {
return target, fnName, oRef.OverloadIDs[0]
}
// Note, this namespaced function name will not appear as a fully qualified name in ASTs
// built and stored before cel-go v0.5.0; however, this functionality did not work at all
// before the v0.5.0 release.
return target, fnName, ""
}
// Parse-only expressions need to handle the same logic as is normally performed at check time,
// but with potentially much less information. The only reliable source of information about
// which functions are configured is the dispatcher.
if target == nil {
// If the user has a parse-only expression, then it should have been configured as such in
// the interpreter dispatcher as it may have been omitted from the checker environment.
for _, qualifiedName := range p.container.ResolveCandidateNames(fnName) {
_, found := p.disp.FindOverload(qualifiedName)
if found {
return nil, qualifiedName, ""
}
}
// It's possible that the overload was not found, but this situation is accounted for in
// the planCall phase; however, the leading dot used for denoting fully-qualified
// namespaced identifiers must be stripped, as all declarations already use fully-qualified
// names. This stripping behavior is handled automatically by the ResolveCandidateNames
// call.
return target, stripLeadingDot(fnName), ""
}
// Handle the situation where the function target actually indicates a qualified function name.
qualifiedPrefix, maybeQualified := p.toQualifiedName(target)
if maybeQualified {
maybeQualifiedName := qualifiedPrefix + "." + fnName
for _, qualifiedName := range p.container.ResolveCandidateNames(maybeQualifiedName) {
_, found := p.disp.FindOverload(qualifiedName)
if found {
// Clear the target to ensure the proper arity is used for finding the
// implementation.
return nil, qualifiedName, ""
}
}
}
// In the default case, the function is exactly as it was advertised: a receiver call on with
// an expression-based target with the given simple function name.
return target, fnName, ""
}
// 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),
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
}
eAttr, ok = decAttr.(InterpretableAttribute)
if !ok {
return nil, fmt.Errorf("invalid attribute decoration: %v(%T)", decAttr, decAttr)
}
return eAttr, nil
}
// toQualifiedName converts an expression AST into a qualified name if possible, with a boolean
// 'found' value that indicates if the conversion is successful.
func (p *planner) toQualifiedName(operand ast.Expr) (string, bool) {
// If the checker identified the expression as an attribute by the type-checker, then it can't
// possibly be part of qualified name in a namespace.
_, isAttr := p.refMap[operand.ID()]
if isAttr {
return "", false
}
// Since functions cannot be both namespaced and receiver functions, if the operand is not an
// qualified variable name, return the (possibly) qualified name given the expressions.
switch operand.Kind() {
case ast.IdentKind:
id := operand.AsIdent()
return id, true
case ast.SelectKind:
sel := operand.AsSelect()
// Test only expressions are not valid as qualified names.
if sel.IsTestOnly() {
return "", false
}
if qual, found := p.toQualifiedName(sel.Operand()); found {
return qual + "." + sel.FieldName(), true
}
}
return "", false
}
func stripLeadingDot(name string) string {
if strings.HasPrefix(name, ".") {
return name[1:]
}
return name
}

View File

@ -0,0 +1,543 @@
// Copyright 2018 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 (
"github.com/google/cel-go/common/ast"
"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"
)
type astPruner struct {
ast.ExprFactory
expr ast.Expr
macroCalls map[int64]ast.Expr
state EvalState
nextExprID int64
}
// TODO Consider having a separate walk of the AST that finds common
// subexpressions. This can be called before or after constant folding to find
// common subexpressions.
// PruneAst prunes the given AST based on the given EvalState and generates a new AST.
// Given AST is copied on write and a new AST is returned.
// Couple of typical use cases this interface would be:
//
// A)
// 1) Evaluate expr with some unknowns,
// 2) If result is unknown:
//
// a) PruneAst
// b) Goto 1
//
// Functional call results which are known would be effectively cached across
// iterations.
//
// B)
// 1) Compile the expression (maybe via a service and maybe after checking a
//
// compiled expression does not exists in local cache)
//
// 2) Prepare the environment and the interpreter. Activation might be empty.
// 3) Eval the expression. This might return unknown or error or a concrete
//
// value.
//
// 4) PruneAst
// 4) Maybe cache the expression
// This is effectively constant folding the expression. How the environment is
// prepared in step 2 is flexible. For example, If the caller caches the
// 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 ast.Expr, macroCalls map[int64]ast.Expr, state EvalState) *ast.AST {
pruneState := NewEvalState()
for _, id := range state.IDs() {
v, _ := state.Value(id)
pruneState.SetValue(id, v)
}
pruner := &astPruner{
ExprFactory: ast.NewExprFactory(),
expr: expr,
macroCalls: macroCalls,
state: pruneState,
nextExprID: getMaxID(expr)}
newExpr, _ := pruner.maybePrune(expr)
newInfo := ast.NewSourceInfo(nil)
for id, call := range pruner.macroCalls {
newInfo.SetMacroCall(id, call)
}
return ast.NewAST(newExpr, newInfo)
}
func (p *astPruner) maybeCreateLiteral(id int64, val ref.Val) (ast.Expr, bool) {
switch v := val.(type) {
case types.Bool, types.Bytes, types.Double, types.Int, types.Null, types.String, types.Uint:
p.state.SetValue(id, val)
return p.NewLiteral(id, val), true
case types.Duration:
p.state.SetValue(id, val)
durationString := v.ConvertToType(types.StringType).(types.String)
return p.NewCall(id, overloads.TypeConvertDuration, p.NewLiteral(p.nextID(), durationString)), true
case types.Timestamp:
timestampString := v.ConvertToType(types.StringType).(types.String)
return p.NewCall(id, overloads.TypeConvertTimestamp, p.NewLiteral(p.nextID(), timestampString)), true
}
// Attempt to build a list literal.
if list, isList := val.(traits.Lister); isList {
sz := list.Size().(types.Int)
elemExprs := make([]ast.Expr, sz)
for i := types.Int(0); i < sz; i++ {
elem := list.Get(i)
if types.IsUnknownOrError(elem) {
return nil, false
}
elemExpr, ok := p.maybeCreateLiteral(p.nextID(), elem)
if !ok {
return nil, false
}
elemExprs[i] = elemExpr
}
p.state.SetValue(id, val)
return p.NewList(id, elemExprs, []int32{}), true
}
// Create a map literal if possible.
if mp, isMap := val.(traits.Mapper); isMap {
it := mp.Iterator()
entries := make([]ast.EntryExpr, mp.Size().(types.Int))
i := 0
for it.HasNext() != types.False {
key := it.Next()
val := mp.Get(key)
if types.IsUnknownOrError(key) || types.IsUnknownOrError(val) {
return nil, false
}
keyExpr, ok := p.maybeCreateLiteral(p.nextID(), key)
if !ok {
return nil, false
}
valExpr, ok := p.maybeCreateLiteral(p.nextID(), val)
if !ok {
return nil, false
}
entry := p.NewMapEntry(p.nextID(), keyExpr, valExpr, false)
entries[i] = entry
i++
}
p.state.SetValue(id, val)
return p.NewMap(id, entries), true
}
// TODO(issues/377) To construct message literals, the type provider will need to support
// the enumeration the fields for a given message.
return nil, false
}
func (p *astPruner) maybePruneOptional(elem ast.Expr) (ast.Expr, bool) {
elemVal, found := p.value(elem.ID())
if found && elemVal.Type() == types.OptionalType {
opt := elemVal.(*types.Optional)
if !opt.HasValue() {
return nil, true
}
if newElem, pruned := p.maybeCreateLiteral(elem.ID(), opt.GetValue()); pruned {
return newElem, true
}
}
return elem, false
}
func (p *astPruner) maybePruneIn(node ast.Expr) (ast.Expr, bool) {
// elem in list
call := node.AsCall()
val, exists := p.maybeValue(call.Args()[1].ID())
if !exists {
return nil, false
}
if sz, ok := val.(traits.Sizer); ok && sz.Size() == types.IntZero {
return p.maybeCreateLiteral(node.ID(), types.False)
}
return nil, false
}
func (p *astPruner) maybePruneLogicalNot(node ast.Expr) (ast.Expr, bool) {
call := node.AsCall()
arg := call.Args()[0]
val, exists := p.maybeValue(arg.ID())
if !exists {
return nil, false
}
if b, ok := val.(types.Bool); ok {
return p.maybeCreateLiteral(node.ID(), !b)
}
return nil, false
}
func (p *astPruner) maybePruneOr(node ast.Expr) (ast.Expr, bool) {
call := node.AsCall()
// 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.Args()[0].ID()); exists {
if v == types.True {
return p.maybeCreateLiteral(node.ID(), types.True)
}
return call.Args()[1], true
}
if v, exists := p.maybeValue(call.Args()[1].ID()); exists {
if v == types.True {
return p.maybeCreateLiteral(node.ID(), types.True)
}
return call.Args()[0], true
}
return nil, false
}
func (p *astPruner) maybePruneAnd(node ast.Expr) (ast.Expr, bool) {
call := node.AsCall()
// 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.Args()[0].ID()); exists {
if v == types.False {
return p.maybeCreateLiteral(node.ID(), types.False)
}
return call.Args()[1], true
}
if v, exists := p.maybeValue(call.Args()[1].ID()); exists {
if v == types.False {
return p.maybeCreateLiteral(node.ID(), types.False)
}
return call.Args()[0], true
}
return nil, false
}
func (p *astPruner) maybePruneConditional(node ast.Expr) (ast.Expr, bool) {
call := node.AsCall()
cond, exists := p.maybeValue(call.Args()[0].ID())
if !exists {
return nil, false
}
if cond.Value().(bool) {
return call.Args()[1], true
}
return call.Args()[2], true
}
func (p *astPruner) maybePruneFunction(node ast.Expr) (ast.Expr, bool) {
if _, exists := p.value(node.ID()); !exists {
return nil, false
}
call := node.AsCall()
if call.FunctionName() == operators.LogicalOr {
return p.maybePruneOr(node)
}
if call.FunctionName() == operators.LogicalAnd {
return p.maybePruneAnd(node)
}
if call.FunctionName() == operators.Conditional {
return p.maybePruneConditional(node)
}
if call.FunctionName() == operators.In {
return p.maybePruneIn(node)
}
if call.FunctionName() == operators.LogicalNot {
return p.maybePruneLogicalNot(node)
}
return nil, false
}
func (p *astPruner) maybePrune(node ast.Expr) (ast.Expr, bool) {
return p.prune(node)
}
func (p *astPruner) prune(node ast.Expr) (ast.Expr, bool) {
if node == nil {
return node, false
}
val, valueExists := p.maybeValue(node.ID())
if valueExists {
if newNode, ok := p.maybeCreateLiteral(node.ID(), val); ok {
delete(p.macroCalls, node.ID())
return newNode, true
}
}
if macro, found := p.macroCalls[node.ID()]; found {
// Ensure that intermediate values for the comprehension are cleared during pruning
if node.Kind() == ast.ComprehensionKind {
compre := node.AsComprehension()
visit(macro, clearIterVarVisitor(compre.IterVar(), p.state))
}
// prune the expression in terms of the macro call instead of the expanded form.
if newMacro, pruned := p.prune(macro); pruned {
p.macroCalls[node.ID()] = 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.Kind() {
case ast.SelectKind:
sel := node.AsSelect()
if operand, isPruned := p.maybePrune(sel.Operand()); isPruned {
if sel.IsTestOnly() {
return p.NewPresenceTest(node.ID(), operand, sel.FieldName()), true
}
return p.NewSelect(node.ID(), operand, sel.FieldName()), true
}
case ast.CallKind:
argsPruned := false
call := node.AsCall()
args := call.Args()
newArgs := make([]ast.Expr, len(args))
for i, a := range args {
newArgs[i] = a
if arg, isPruned := p.maybePrune(a); isPruned {
argsPruned = true
newArgs[i] = arg
}
}
if !call.IsMemberFunction() {
newCall := p.NewCall(node.ID(), call.FunctionName(), newArgs...)
if prunedCall, isPruned := p.maybePruneFunction(newCall); isPruned {
return prunedCall, true
}
return newCall, argsPruned
}
newTarget := call.Target()
targetPruned := false
if prunedTarget, isPruned := p.maybePrune(call.Target()); isPruned {
targetPruned = true
newTarget = prunedTarget
}
newCall := p.NewMemberCall(node.ID(), call.FunctionName(), newTarget, newArgs...)
if prunedCall, isPruned := p.maybePruneFunction(newCall); isPruned {
return prunedCall, true
}
return newCall, targetPruned || argsPruned
case ast.ListKind:
l := node.AsList()
elems := l.Elements()
optIndices := l.OptionalIndices()
optIndexMap := map[int32]bool{}
for _, i := range optIndices {
optIndexMap[i] = true
}
newOptIndexMap := make(map[int32]bool, len(optIndexMap))
newElems := make([]ast.Expr, 0, len(elems))
var listPruned bool
prunedIdx := 0
for i, elem := range elems {
_, isOpt := optIndexMap[int32(i)]
if isOpt {
newElem, pruned := p.maybePruneOptional(elem)
if pruned {
listPruned = 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)
listPruned = true
} else {
newElems = append(newElems, elem)
}
prunedIdx++
}
optIndices = make([]int32, len(newOptIndexMap))
idx := 0
for i := range newOptIndexMap {
optIndices[idx] = i
idx++
}
if listPruned {
return p.NewList(node.ID(), newElems, optIndices), true
}
case ast.MapKind:
var mapPruned bool
m := node.AsMap()
entries := m.Entries()
newEntries := make([]ast.EntryExpr, len(entries))
for i, entry := range entries {
newEntries[i] = entry
e := entry.AsMapEntry()
newKey, keyPruned := p.maybePrune(e.Key())
newValue, valuePruned := p.maybePrune(e.Value())
if !keyPruned && !valuePruned {
continue
}
mapPruned = true
newEntry := p.NewMapEntry(entry.ID(), newKey, newValue, e.IsOptional())
newEntries[i] = newEntry
}
if mapPruned {
return p.NewMap(node.ID(), newEntries), true
}
case ast.StructKind:
var structPruned bool
obj := node.AsStruct()
fields := obj.Fields()
newFields := make([]ast.EntryExpr, len(fields))
for i, field := range fields {
newFields[i] = field
f := field.AsStructField()
newValue, prunedValue := p.maybePrune(f.Value())
if !prunedValue {
continue
}
structPruned = true
newEntry := p.NewStructField(field.ID(), f.Name(), newValue, f.IsOptional())
newFields[i] = newEntry
}
if structPruned {
return p.NewStruct(node.ID(), obj.TypeName(), newFields), true
}
case ast.ComprehensionKind:
compre := node.AsComprehension()
// 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.maybePrune(compre.IterRange()); pruned {
return p.NewComprehension(
node.ID(),
newRange,
compre.IterVar(),
compre.AccuVar(),
compre.AccuInit(),
compre.LoopCondition(),
compre.LoopStep(),
compre.Result(),
), true
}
}
return node, false
}
func (p *astPruner) value(id int64) (ref.Val, bool) {
val, found := p.state.Value(id)
return val, (found && val != nil)
}
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 {
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 ast.Expr)
// visitEntry is called before entering the key, value of a map/struct entry.
visitEntry func(entry ast.EntryExpr)
}
func getMaxID(expr ast.Expr) int64 {
maxID := int64(1)
visit(expr, maxIDVisitor(&maxID))
return maxID
}
func clearIterVarVisitor(varName string, state EvalState) astVisitor {
return astVisitor{
visitExpr: func(e ast.Expr) {
if e.Kind() == ast.IdentKind && e.AsIdent() == varName {
state.SetValue(e.ID(), nil)
}
},
}
}
func maxIDVisitor(maxID *int64) astVisitor {
return astVisitor{
visitExpr: func(e ast.Expr) {
if e.ID() >= *maxID {
*maxID = e.ID() + 1
}
},
visitEntry: func(e ast.EntryExpr) {
if e.ID() >= *maxID {
*maxID = e.ID() + 1
}
},
}
}
func visit(expr ast.Expr, visitor astVisitor) {
exprs := []ast.Expr{expr}
for len(exprs) != 0 {
e := exprs[0]
if visitor.visitExpr != nil {
visitor.visitExpr(e)
}
exprs = exprs[1:]
switch e.Kind() {
case ast.SelectKind:
exprs = append(exprs, e.AsSelect().Operand())
case ast.CallKind:
call := e.AsCall()
if call.Target() != nil {
exprs = append(exprs, call.Target())
}
exprs = append(exprs, call.Args()...)
case ast.ComprehensionKind:
compre := e.AsComprehension()
exprs = append(exprs,
compre.IterRange(),
compre.AccuInit(),
compre.LoopCondition(),
compre.LoopStep(),
compre.Result())
case ast.ListKind:
list := e.AsList()
exprs = append(exprs, list.Elements()...)
case ast.MapKind:
for _, entry := range e.AsMap().Entries() {
e := entry.AsMapEntry()
if visitor.visitEntry != nil {
visitor.visitEntry(entry)
}
exprs = append(exprs, e.Key())
exprs = append(exprs, e.Value())
}
case ast.StructKind:
for _, entry := range e.AsStruct().Fields() {
f := entry.AsStructField()
if visitor.visitEntry != nil {
visitor.visitEntry(entry)
}
exprs = append(exprs, f.Value())
}
}
}
}

View File

@ -0,0 +1,316 @@
// Copyright 2022 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"
"github.com/google/cel-go/common"
"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"
)
// WARNING: Any changes to cost calculations in this file require a corresponding change in checker/cost.go
// ActualCostEstimator provides function call cost estimations at runtime
// CallCost returns an estimated cost for the function overload invocation with the given args, or nil if it has no
// estimate to provide. CEL attempts to provide reasonable estimates for its standard function library, so CallCost
// should typically not need to provide an estimate for CELs standard function.
type ActualCostEstimator interface {
CallCost(function, overloadID string, args []ref.Val, result ref.Val) *uint64
}
// CostObserver provides an observer that tracks runtime cost.
func CostObserver(tracker *CostTracker) EvalObserver {
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
// and enable the below pop. Once enabled this can case can be collapsed into the Qualifier case.
tracker.cost++
case InterpretableConst:
// zero cost
case InterpretableAttribute:
switch a := t.Attr().(type) {
case *conditionalAttribute:
// Ternary has no direct cost. All cost is from the conditional and the true/false branch expressions.
tracker.stack.drop(a.falsy.ID(), a.truthy.ID(), a.expr.ID())
default:
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())
// While the field names are identical, the boolean operation eval structs do not share an interface and so
// must be handled individually.
case *evalOr:
for _, term := range t.terms {
tracker.stack.drop(term.ID())
}
case *evalAnd:
for _, term := range t.terms {
tracker.stack.drop(term.ID())
}
case *evalExhaustiveOr:
for _, term := range t.terms {
tracker.stack.drop(term.ID())
}
case *evalExhaustiveAnd:
for _, term := range t.terms {
tracker.stack.drop(term.ID())
}
case *evalFold:
tracker.stack.drop(t.iterRange.ID())
case Qualifier:
tracker.cost++
case InterpretableCall:
if argVals, ok := tracker.stack.dropArgs(t.Args()); ok {
tracker.cost += tracker.costCall(t, argVals, val)
}
case InterpretableConstructor:
tracker.stack.dropArgs(t.InitVals())
switch t.Type() {
case types.ListType:
tracker.cost += common.ListCreateBaseCost
case types.MapType:
tracker.cost += common.MapCreateBaseCost
default:
tracker.cost += common.StructCreateBaseCost
}
}
tracker.stack.push(val, id)
if tracker.Limit != nil && tracker.cost > *tracker.Limit {
panic(EvalCancelledError{Cause: CostLimitExceeded, Message: "operation cancelled: actual cost limit exceeded"})
}
}
return observer
}
// 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,
overloadTrackers: map[string]FunctionTracker{},
presenceTestHasCost: true,
}
for _, opt := range opts {
err := opt(tracker)
if err != nil {
return nil, err
}
}
return tracker, nil
}
// OverloadCostTracker binds an overload ID to a runtime FunctionTracker implementation.
//
// OverloadCostTracker instances augment or override ActualCostEstimator decisions, allowing for versioned and/or
// optional cost tracking changes.
func OverloadCostTracker(overloadID string, fnTracker FunctionTracker) CostTrackerOption {
return func(tracker *CostTracker) error {
tracker.overloadTrackers[overloadID] = fnTracker
return nil
}
}
// FunctionTracker computes the actual cost of evaluating the functions with the given arguments and result.
type FunctionTracker func(args []ref.Val, result ref.Val) *uint64
// CostTracker represents the information needed for tracking runtime cost.
type CostTracker struct {
Estimator ActualCostEstimator
overloadTrackers map[string]FunctionTracker
Limit *uint64
presenceTestHasCost bool
cost uint64
stack refValStack
}
// ActualCost returns the runtime cost
func (c *CostTracker) ActualCost() uint64 {
return c.cost
}
func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result ref.Val) uint64 {
var cost uint64
if len(c.overloadTrackers) != 0 {
if tracker, found := c.overloadTrackers[call.OverloadID()]; found {
callCost := tracker(args, result)
if callCost != nil {
cost += *callCost
return cost
}
}
}
if c.Estimator != nil {
callCost := c.Estimator.CallCost(call.Function(), call.OverloadID(), args, result)
if callCost != nil {
cost += *callCost
return cost
}
}
// if user didn't specify, the default way of calculating runtime cost would be used.
// 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, overloads.ExtQuoteString, overloads.ExtFormatString:
cost += uint64(math.Ceil(float64(c.actualSize(args[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.
// We just assume all list containment checks are O(n).
cost += c.actualSize(args[1])
// O(min(m, n)) functions
case overloads.LessString, overloads.GreaterString, overloads.LessEqualsString, overloads.GreaterEqualsString,
overloads.LessBytes, overloads.GreaterBytes, overloads.LessEqualsBytes, overloads.GreaterEqualsBytes,
overloads.Equals, overloads.NotEquals:
// When we check the equality of 2 scalar values (e.g. 2 integers, 2 floating-point numbers, 2 booleans etc.),
// the CostTracker.actualSize() function by definition returns 1 for each operand, resulting in an overall cost
// of 1.
lhsSize := c.actualSize(args[0])
rhsSize := c.actualSize(args[1])
minSize := lhsSize
if rhsSize < minSize {
minSize = rhsSize
}
cost += uint64(math.Ceil(float64(minSize) * common.StringTraversalCostFactor))
// O(m+n) functions
case overloads.AddString, overloads.AddBytes:
// In the worst case scenario, we would need to reallocate a new backing store and copy both operands over.
cost += uint64(math.Ceil(float64(c.actualSize(args[0])+c.actualSize(args[1])) * common.StringTraversalCostFactor))
// O(nm) functions
case overloads.MatchesString:
// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
// Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0
// in case where string is empty but regex is still expensive.
strCost := uint64(math.Ceil((1.0 + float64(c.actualSize(args[0]))) * common.StringTraversalCostFactor))
// We don't know how many expressions are in the regex, just the string length (a huge
// improvement here would be to somehow get a count the number of expressions in the regex or
// how many states are in the regex state machine and use that to measure regex cost).
// For now, we're making a guess that each expression in a regex is typically at least 4 chars
// in length.
regexCost := uint64(math.Ceil(float64(c.actualSize(args[1])) * common.RegexStringLengthCostFactor))
cost += strCost * regexCost
case overloads.ContainsString:
strCost := uint64(math.Ceil(float64(c.actualSize(args[0])) * common.StringTraversalCostFactor))
substrCost := uint64(math.Ceil(float64(c.actualSize(args[1])) * common.StringTraversalCostFactor))
cost += strCost * substrCost
default:
// The following operations are assumed to have O(1) complexity.
// - AddList due to the implementation. Index lookup can be O(c) the
// number of concatenated lists, but we don't track that is cost calculations.
// - Conversions, since none perform a traversal of a type of unbound length.
// - Computing the size of strings, byte sequences, lists and maps.
// - Logical operations and all operators on fixed width scalars (comparisons, equality)
// - Any functions that don't have a declared cost either here or in provided ActualCostEstimator.
cost++
}
return cost
}
// actualSize returns the size of value
func (c *CostTracker) actualSize(value ref.Val) uint64 {
if sz, ok := value.(traits.Sizer); ok {
return uint64(sz.Size().(types.Int))
}
return 1
}
type stackVal struct {
Val ref.Val
ID int64
}
// refValStack keeps track of values of the stack for cost calculation purposes
type refValStack []stackVal
func (s *refValStack) push(val ref.Val, id int64) {
value := stackVal{Val: val, ID: id}
*s = append(*s, value)
}
// TODO: Allowing drop and dropArgs to remove stack items above the IDs they are provided is a workaround. drop and dropArgs
// should find and remove only the stack items matching the provided IDs once all attributes are properly pushed and popped from stack.
// drop searches the stack for each ID and removes the ID and all stack items above it.
// If none of the IDs are found, the stack is not modified.
// WARNING: It is possible for multiple expressions with the same ID to exist (due to how macros are implemented) so it's
// possible that a dropped ID will remain on the stack. They should be removed when IDs on the stack are popped.
func (s *refValStack) drop(ids ...int64) {
for _, id := range ids {
for idx := len(*s) - 1; idx >= 0; idx-- {
if (*s)[idx].ID == id {
*s = (*s)[:idx]
break
}
}
}
}
// dropArgs searches the stack for all the args by their IDs, accumulates their associated ref.Vals and drops any
// stack items above any of the arg IDs. If any of the IDs are not found the stack, false is returned.
// Args are assumed to be found in the stack in reverse order, i.e. the last arg is expected to be found highest in
// the stack.
// WARNING: It is possible for multiple expressions with the same ID to exist (due to how macros are implemented) so it's
// possible that a dropped ID will remain on the stack. They should be removed when IDs on the stack are popped.
func (s *refValStack) dropArgs(args []Interpretable) ([]ref.Val, bool) {
result := make([]ref.Val, len(args))
argloop:
for nIdx := len(args) - 1; nIdx >= 0; nIdx-- {
for idx := len(*s) - 1; idx >= 0; idx-- {
if (*s)[idx].ID == args[nIdx].ID() {
el := (*s)[idx]
*s = (*s)[:idx]
result[nIdx] = el.Val
continue argloop
}
}
return nil, false
}
return result, true
}