Bump CEL to 0.11.2

This commit is contained in:
Joe Betz
2022-03-24 11:34:14 -04:00
parent e7845861a5
commit 4c90653d19
139 changed files with 2688 additions and 1637 deletions

View File

@@ -44,7 +44,9 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"cel_example_test.go",
"cel_test.go",
"env_test.go",
"io_test.go",
],
data = [
@@ -66,6 +68,7 @@ go_test(
"//test/proto3pb:go_default_library",
"@io_bazel_rules_go//proto/wkt:descriptor_go_proto",
"@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/structpb:go_default_library",
],
)

View File

@@ -32,9 +32,7 @@ import (
)
// Source interface representing a user-provided expression.
type Source interface {
common.Source
}
type Source = common.Source
// Ast representing the checked or unchecked expression, its source, and related metadata such as
// source position information.
@@ -56,7 +54,7 @@ func (ast *Ast) IsChecked() bool {
return ast.typeMap != nil && len(ast.typeMap) > 0
}
// SourceInfo returns character offset and newling position information about expression elements.
// SourceInfo returns character offset and newline position information about expression elements.
func (ast *Ast) SourceInfo() *exprpb.SourceInfo {
return ast.info
}
@@ -98,6 +96,7 @@ type Env struct {
chk *checker.Env
chkErr error
chkOnce sync.Once
chkOpts []checker.Option
// Program options tied to the environment
progOpts []ProgramOption
@@ -110,8 +109,16 @@ type Env struct {
// See the EnvOption helper functions for the options that can be used to configure the
// environment.
func NewEnv(opts ...EnvOption) (*Env, error) {
stdOpts := append([]EnvOption{StdLib()}, opts...)
return NewCustomEnv(stdOpts...)
// Extend the statically configured standard environment, disabling eager validation to ensure
// the cost of setup for the environment is still just as cheap as it is in v0.11.x and earlier
// releases. The user provided options can easily re-enable the eager validation as they are
// processed after this default option.
stdOpts := append([]EnvOption{EagerlyValidateDeclarations(false)}, opts...)
env, err := getStdEnv()
if err != nil {
return nil, err
}
return env.Extend(stdOpts...)
}
// NewCustomEnv creates a custom program environment which is not automatically configured with the
@@ -152,25 +159,8 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
pe, _ := AstToParsedExpr(ast)
// Construct the internal checker env, erroring if there is an issue adding the declarations.
e.chkOnce.Do(func() {
ce, err := checker.NewEnv(e.Container, e.provider,
checker.HomogeneousAggregateLiterals(
e.HasFeature(featureDisableDynamicAggregateLiterals)),
checker.CrossTypeNumericComparisons(
e.HasFeature(featureCrossTypeNumericComparisons)))
if err != nil {
e.chkErr = err
return
}
err = ce.Add(e.declarations...)
if err != nil {
e.chkErr = err
return
}
e.chk = ce
})
// The once call will ensure that this value is set or nil for all invocations.
if e.chkErr != nil {
err := e.initChecker()
if err != nil {
errs := common.NewErrors(ast.Source())
errs.ReportError(common.NoLocation, e.chkErr.Error())
return nil, NewIssues(errs)
@@ -210,7 +200,7 @@ func (e *Env) Compile(txt string) (*Ast, *Issues) {
// issues discovered during Check.
//
// Note, for parse-only uses of CEL use Parse.
func (e *Env) CompileSource(src common.Source) (*Ast, *Issues) {
func (e *Env) CompileSource(src Source) (*Ast, *Issues) {
ast, iss := e.ParseSource(src)
if iss.Err() != nil {
return nil, iss
@@ -233,11 +223,29 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
if e.chkErr != nil {
return nil, e.chkErr
}
// Copy slices.
decsCopy := make([]*exprpb.Decl, len(e.declarations))
// The type-checker is configured with Declarations. The declarations may either be provided
// as options which have not yet been validated, or may come from a previous checker instance
// whose types have already been validated.
chkOptsCopy := make([]checker.Option, len(e.chkOpts))
copy(chkOptsCopy, e.chkOpts)
// Copy the declarations if needed.
decsCopy := []*exprpb.Decl{}
if e.chk != nil {
// If the type-checker has already been instantiated, then the e.declarations have been
// valdiated within the chk instance.
chkOptsCopy = append(chkOptsCopy, checker.ValidatedDeclarations(e.chk))
} else {
// If the type-checker has not been instantiated, ensure the unvalidated declarations are
// provided to the extended Env instance.
decsCopy = make([]*exprpb.Decl, len(e.declarations))
copy(decsCopy, e.declarations)
}
// Copy macros and program options
macsCopy := make([]parser.Macro, len(e.macros))
progOptsCopy := make([]ProgramOption, len(e.progOpts))
copy(decsCopy, e.declarations)
copy(macsCopy, e.macros)
copy(progOptsCopy, e.progOpts)
@@ -281,6 +289,7 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
adapter: adapter,
features: featuresCopy,
provider: provider,
chkOpts: chkOptsCopy,
}
return ext.configure(opts)
}
@@ -294,7 +303,7 @@ func (e *Env) HasFeature(flag int) bool {
// Parse parses the input expression value `txt` to a Ast and/or a set of Issues.
//
// This form of Parse creates a common.Source value for the input `txt` and forwards to the
// This form of Parse creates a Source value for the input `txt` and forwards to the
// ParseSource method.
func (e *Env) Parse(txt string) (*Ast, *Issues) {
src := common.NewTextSource(txt)
@@ -308,7 +317,7 @@ func (e *Env) Parse(txt string) (*Ast, *Issues) {
//
// It is possible to have both non-nil Ast and Issues values returned from this call; however,
// the mere presence of an Ast does not imply that it is valid for use.
func (e *Env) ParseSource(src common.Source) (*Ast, *Issues) {
func (e *Env) ParseSource(src Source) (*Ast, *Issues) {
res, errs := e.prsr.Parse(src)
if len(errs.GetErrors()) > 0 {
return nil, &Issues{errs: errs}
@@ -316,7 +325,7 @@ func (e *Env) ParseSource(src common.Source) (*Ast, *Issues) {
// Manually create the Ast to ensure that the text source information is propagated on
// subsequent calls to Check.
return &Ast{
source: Source(src),
source: src,
expr: res.GetExpr(),
info: res.GetSourceInfo()}, nil
}
@@ -426,6 +435,8 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) {
return nil, err
}
}
// Configure the parser.
prsrOpts := []parser.Option{parser.Macros(e.macros...)}
if e.HasFeature(featureEnableMacroCallTracking) {
prsrOpts = append(prsrOpts, parser.PopulateMacroCalls(true))
@@ -434,9 +445,44 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) {
if err != nil {
return nil, err
}
// The simplest way to eagerly validate declarations on environment creation is to compile
// a dummy program and check for the presence of e.chkErr being non-nil.
if e.HasFeature(featureEagerlyValidateDeclarations) {
err := e.initChecker()
if err != nil {
return nil, err
}
}
return e, nil
}
func (e *Env) initChecker() error {
e.chkOnce.Do(func() {
chkOpts := []checker.Option{}
chkOpts = append(chkOpts, e.chkOpts...)
chkOpts = append(chkOpts,
checker.HomogeneousAggregateLiterals(
e.HasFeature(featureDisableDynamicAggregateLiterals)),
checker.CrossTypeNumericComparisons(
e.HasFeature(featureCrossTypeNumericComparisons)))
ce, err := checker.NewEnv(e.Container, e.provider, chkOpts...)
if err != nil {
e.chkErr = err
return
}
err = ce.Add(e.declarations...)
if err != nil {
e.chkErr = err
return
}
e.chk = ce
})
return e.chkErr
}
// Issues defines methods for inspecting the error details of parse and check calls.
//
// Note: in the future, non-fatal warnings and notices may be inspectable via the Issues struct.
@@ -488,3 +534,17 @@ func (i *Issues) String() string {
}
return i.errs.ToDisplayString()
}
// getStdEnv lazy initializes the CEL standard environment.
func getStdEnv() (*Env, error) {
stdEnvInit.Do(func() {
stdEnv, stdEnvErr = NewCustomEnv(StdLib(), EagerlyValidateDeclarations(true))
})
return stdEnv, stdEnvErr
}
var (
stdEnvInit sync.Once
stdEnv *Env
stdEnvErr error
)

View File

@@ -44,7 +44,7 @@ func CheckedExprToAst(checkedExpr *exprpb.CheckedExpr) *Ast {
// through future calls.
//
// Prefer CheckedExprToAst if loading expressions from storage.
func CheckedExprToAstWithSource(checkedExpr *exprpb.CheckedExpr, src common.Source) *Ast {
func CheckedExprToAstWithSource(checkedExpr *exprpb.CheckedExpr, src Source) *Ast {
refMap := checkedExpr.GetReferenceMap()
if refMap == nil {
refMap = map[int64]*exprpb.Reference{}
@@ -96,7 +96,7 @@ func ParsedExprToAst(parsedExpr *exprpb.ParsedExpr) *Ast {
// expression, or if you need to separately check a subset of an expression.
//
// Prefer ParsedExprToAst if loading expressions from storage.
func ParsedExprToAstWithSource(parsedExpr *exprpb.ParsedExpr, src common.Source) *Ast {
func ParsedExprToAstWithSource(parsedExpr *exprpb.ParsedExpr, src Source) *Ast {
si := parsedExpr.GetSourceInfo()
if si == nil {
si = &exprpb.SourceInfo{}

View File

@@ -20,14 +20,14 @@ import (
"github.com/google/cel-go/parser"
)
// Library provides a collection of EnvOption and ProgramOption values used to confiugre a CEL
// Library provides a collection of EnvOption and ProgramOption values used to configure a CEL
// environment for a particular use case or with a related set of functionality.
//
// Note, the ProgramOption values provided by a library are expected to be static and not vary
// between calls to Env.Program(). If there is a need for such dynamic configuration, prefer to
// configure these options outside the Library and within the Env.Program() call directly.
type Library interface {
// CompileOptions returns a collection of funcitional options for configuring the Parse / Check
// CompileOptions returns a collection of functional options for configuring the Parse / Check
// environment.
CompileOptions() []EnvOption

View File

@@ -53,6 +53,10 @@ const (
// Enable the use of cross-type numeric comparisons at the type-checker.
featureCrossTypeNumericComparisons
// Enable eager validation of declarations to ensure that Env values created
// with `Extend` inherit a validated list of declarations from the parent Env.
featureEagerlyValidateDeclarations
)
// EnvOption is a functional interface for configuring the environment.
@@ -103,6 +107,18 @@ func Declarations(decls ...*exprpb.Decl) EnvOption {
}
}
// EagerlyValidateDeclarations ensures that any collisions between configured declarations are caught
// at the time of the `NewEnv` call.
//
// Eagerly validating declarations is also useful for bootstrapping a base `cel.Env` value.
// Calls to base `Env.Extend()` will be significantly faster when declarations are eagerly validated
// as declarations will be collision-checked at most once and only incrementally by way of `Extend`
//
// Disabled by default as not all environments are used for type-checking.
func EagerlyValidateDeclarations(enabled bool) EnvOption {
return features(featureEagerlyValidateDeclarations, enabled)
}
// HomogeneousAggregateLiterals option ensures that list and map literal entry types must agree
// during type-checking.
//

View File

@@ -49,7 +49,7 @@ type Program interface {
// to support cancellation and timeouts. This method must be used in conjunction with the
// InterruptCheckFrequency() option for cancellation interrupts to be impact evaluation.
//
// The vars value may eitehr be an `interpreter.Activation` or `map[string]interface{}`.
// The vars value may either be an `interpreter.Activation` or `map[string]interface{}`.
//
// The output contract for `ContextEval` is otherwise identical to the `Eval` method.
ContextEval(context.Context, interface{}) (ref.Val, *EvalDetails, error)

View File

@@ -168,11 +168,9 @@ func (c *checker) checkSelect(e *exprpb.Expr) {
if found {
ident := c.env.LookupIdent(qname)
if ident != nil {
if sel.TestOnly {
c.errors.expressionDoesNotSelectField(c.location(e))
c.setType(e, decls.Bool)
return
}
// We don't check for a TestOnly expression here since the `found` result is
// always going to be false for TestOnly expressions.
// Rewrite the node to be a variable reference to the resolved fully-qualified
// variable name.
c.setType(e, ident.GetIdent().Type)
@@ -208,7 +206,7 @@ func (c *checker) checkSelect(e *exprpb.Expr) {
resultType = fieldType.Type
}
case kindTypeParam:
// Set the operand type to DYN to prevent assignment to a potentionally incorrect type
// Set the operand type to DYN to prevent assignment to a potentially incorrect type
// at a later point in type-checking. The isAssignable call will update the type
// substitutions for the type param under the covers.
c.isAssignable(decls.Dyn, targetType)
@@ -323,6 +321,12 @@ func (c *checker) resolveOverload(
var resultType *exprpb.Type
var checkedRef *exprpb.Reference
for _, overload := range fn.GetFunction().Overloads {
// Determine whether the overload is currently considered.
if c.env.isOverloadDisabled(overload.GetOverloadId()) {
continue
}
// Ensure the call style for the overload matches.
if (target == nil && overload.IsInstanceFunction) ||
(target != nil && !overload.IsInstanceFunction) {
// not a compatible call style.
@@ -330,26 +334,26 @@ func (c *checker) resolveOverload(
}
overloadType := decls.NewFunctionType(overload.ResultType, overload.Params...)
if len(overload.TypeParams) > 0 {
if len(overload.GetTypeParams()) > 0 {
// Instantiate overload's type with fresh type variables.
substitutions := newMapping()
for _, typePar := range overload.TypeParams {
for _, typePar := range overload.GetTypeParams() {
substitutions.add(decls.NewTypeParamType(typePar), c.newTypeVar())
}
overloadType = substitute(substitutions, overloadType, false)
}
candidateArgTypes := overloadType.GetFunction().ArgTypes
candidateArgTypes := overloadType.GetFunction().GetArgTypes()
if c.isAssignableList(argTypes, candidateArgTypes) {
if checkedRef == nil {
checkedRef = newFunctionReference(overload.OverloadId)
checkedRef = newFunctionReference(overload.GetOverloadId())
} else {
checkedRef.OverloadId = append(checkedRef.OverloadId, overload.OverloadId)
checkedRef.OverloadId = append(checkedRef.OverloadId, overload.GetOverloadId())
}
// First matching overload, determines result type.
fnResultType := substitute(c.mappings,
overloadType.GetFunction().ResultType,
overloadType.GetFunction().GetResultType(),
false)
if resultType == nil {
resultType = fnResultType
@@ -478,7 +482,7 @@ func (c *checker) checkComprehension(e *exprpb.Expr) {
// Ranges over the keys.
varType = rangeType.GetMapType().KeyType
case kindDyn, kindError, kindTypeParam:
// Set the range type to DYN to prevent assignment to a potentionally incorrect type
// Set the range type to DYN to prevent assignment to a potentially incorrect type
// at a later point in type-checking. The isAssignable call will update the type
// substitutions for the type param under the covers.
c.isAssignable(decls.Dyn, rangeType)

View File

@@ -121,7 +121,7 @@ type SizeEstimate struct {
}
// Add adds to another SizeEstimate and returns the sum.
// If add would result in an uint64 overflow, the result is Maxuint64.
// If add would result in an uint64 overflow, the result is math.MaxUint64.
func (se SizeEstimate) Add(sizeEstimate SizeEstimate) SizeEstimate {
return SizeEstimate{
addUint64NoOverflow(se.Min, sizeEstimate.Min),
@@ -130,7 +130,7 @@ func (se SizeEstimate) Add(sizeEstimate SizeEstimate) SizeEstimate {
}
// Multiply multiplies by another SizeEstimate and returns the product.
// If multiply would result in an uint64 overflow, the result is Maxuint64.
// If multiply would result in an uint64 overflow, the result is math.MaxUint64.
func (se SizeEstimate) Multiply(sizeEstimate SizeEstimate) SizeEstimate {
return SizeEstimate{
multiplyUint64NoOverflow(se.Min, sizeEstimate.Min),
@@ -148,7 +148,7 @@ func (se SizeEstimate) MultiplyByCostFactor(costPerUnit float64) CostEstimate {
}
// MultiplyByCost multiplies by the cost and returns the product.
// If multiply would result in an uint64 overflow, the result is Maxuint64.
// If multiply would result in an uint64 overflow, the result is math.MaxUint64.
func (se SizeEstimate) MultiplyByCost(cost CostEstimate) CostEstimate {
return CostEstimate{
multiplyUint64NoOverflow(se.Min, cost.Min),
@@ -175,7 +175,7 @@ type CostEstimate struct {
}
// Add adds the costs and returns the sum.
// If add would result in an uint64 overflow for the min or max, the value is set to Maxuint64.
// If add would result in an uint64 overflow for the min or max, the value is set to math.MaxUint64.
func (ce CostEstimate) Add(cost CostEstimate) CostEstimate {
return CostEstimate{
addUint64NoOverflow(ce.Min, cost.Min),
@@ -184,7 +184,7 @@ func (ce CostEstimate) Add(cost CostEstimate) CostEstimate {
}
// Multiply multiplies by the cost and returns the product.
// If multiply would result in an uint64 overflow, the result is Maxuint64.
// If multiply would result in an uint64 overflow, the result is math.MaxUint64.
func (ce CostEstimate) Multiply(cost CostEstimate) CostEstimate {
return CostEstimate{
multiplyUint64NoOverflow(ce.Min, cost.Min),

View File

@@ -33,6 +33,19 @@ func NewScopes() *Scopes {
}
}
// Copy creates a copy of the current Scopes values, including a copy of its parent if non-nil.
func (s *Scopes) Copy() *Scopes {
cpy := NewScopes()
if s == nil {
return cpy
}
if s.parent != nil {
cpy.parent = s.parent.Copy()
}
cpy.scopes = s.scopes.copy()
return cpy
}
// Push creates a new Scopes value which references the current Scope as its parent.
func (s *Scopes) Push() *Scopes {
return &Scopes{
@@ -80,9 +93,9 @@ func (s *Scopes) FindIdentInScope(name string) *exprpb.Decl {
return nil
}
// AddFunction adds the function Decl to the current scope.
// SetFunction adds the function Decl to the current scope.
// Note: Any previous entry for a function in the current scope with the same name is overwritten.
func (s *Scopes) AddFunction(fn *exprpb.Decl) {
func (s *Scopes) SetFunction(fn *exprpb.Decl) {
s.scopes.functions[fn.Name] = fn
}
@@ -100,13 +113,30 @@ func (s *Scopes) FindFunction(name string) *exprpb.Decl {
}
// Group is a set of Decls that is pushed on or popped off a Scopes as a unit.
// Contains separate namespaces for idenifier and function Decls.
// Contains separate namespaces for identifier and function Decls.
// (Should be named "Scope" perhaps?)
type Group struct {
idents map[string]*exprpb.Decl
functions map[string]*exprpb.Decl
}
// copy creates a new Group instance with a shallow copy of the variables and functions.
// If callers need to mutate the exprpb.Decl definitions for a Function, they should copy-on-write.
func (g *Group) copy() *Group {
cpy := &Group{
idents: make(map[string]*exprpb.Decl, len(g.idents)),
functions: make(map[string]*exprpb.Decl, len(g.functions)),
}
for n, id := range g.idents {
cpy.idents[n] = id
}
for n, fn := range g.functions {
cpy.functions[n] = fn
}
return cpy
}
// newGroup creates a new Group with empty maps for identifiers and functions.
func newGroup() *Group {
return &Group{
idents: make(map[string]*exprpb.Decl),

View File

@@ -18,6 +18,8 @@ import (
"fmt"
"strings"
"google.golang.org/protobuf/proto"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/overloads"
@@ -99,6 +101,9 @@ func NewEnv(container *containers.Container, provider ref.TypeProvider, opts ...
if envOptions.crossTypeNumericComparisons {
filteredOverloadIDs = make(map[string]struct{})
}
if envOptions.validatedDeclarations != nil {
declarations = envOptions.validatedDeclarations.Copy()
}
return &Env{
container: container,
provider: provider,
@@ -117,7 +122,7 @@ func (e *Env) Add(decls ...*exprpb.Decl) error {
case *exprpb.Decl_Ident:
errMsgs = append(errMsgs, e.addIdent(sanitizeIdent(decl)))
case *exprpb.Decl_Function:
errMsgs = append(errMsgs, e.addFunction(sanitizeFunction(decl))...)
errMsgs = append(errMsgs, e.setFunction(sanitizeFunction(decl))...)
}
}
return formatError(errMsgs)
@@ -204,22 +209,22 @@ func (e *Env) addOverload(f *exprpb.Decl, overload *exprpb.Decl_FunctionDecl_Ove
return errMsgs
}
// addFunction adds the function Decl to the Env.
// setFunction adds the function Decl to the Env.
// Adds a function decl if one doesn't already exist, then adds all overloads from the Decl.
// If overload overlaps with an existing overload, adds to the errors in the Env instead.
func (e *Env) addFunction(decl *exprpb.Decl) []errorMsg {
func (e *Env) setFunction(decl *exprpb.Decl) []errorMsg {
current := e.declarations.FindFunction(decl.Name)
if current == nil {
//Add the function declaration without overloads and check the overloads below.
current = decls.NewFunction(decl.Name)
e.declarations.AddFunction(current)
} else {
// Copy on write since we don't know where this original definition came from.
current = proto.Clone(current).(*exprpb.Decl)
}
e.declarations.SetFunction(current)
errorMsgs := make([]errorMsg, 0)
for _, overload := range decl.GetFunction().GetOverloads() {
if _, found := e.filteredOverloadIDs[overload.GetOverloadId()]; found {
continue
}
errorMsgs = append(errorMsgs, e.addOverload(current, overload)...)
}
return errorMsgs
@@ -236,6 +241,12 @@ func (e *Env) addIdent(decl *exprpb.Decl) errorMsg {
return ""
}
// isOverloadDisabled returns whether the overloadID is disabled in the current environment.
func (e *Env) isOverloadDisabled(overloadID string) bool {
_, found := e.filteredOverloadIDs[overloadID]
return found
}
// sanitizeFunction replaces well-known types referenced by message name with their equivalent
// CEL built-in type instances.
func sanitizeFunction(decl *exprpb.Decl) *exprpb.Decl {
@@ -313,6 +324,12 @@ func getObjectWellKnownType(t *exprpb.Type) *exprpb.Type {
return pb.CheckedWellKnowns[t.GetMessageType()]
}
// validatedDeclarations returns a reference to the validated variable and function declaration scope stack.
// must be copied before use.
func (e *Env) validatedDeclarations() *decls.Scopes {
return e.declarations
}
// enterScope creates a new Env instance with a new innermost declaration scope.
func (e *Env) enterScope() *Env {
childDecls := e.declarations.Push()

View File

@@ -29,10 +29,6 @@ func (e *typeErrors) undeclaredReference(l common.Location, container string, na
e.ReportError(l, "undeclared reference to '%s' (in container '%s')", name, container)
}
func (e *typeErrors) expressionDoesNotSelectField(l common.Location) {
e.ReportError(l, "expression does not select a field")
}
func (e *typeErrors) typeDoesNotSupportFieldSelection(l common.Location, t *exprpb.Type) {
e.ReportError(l, "type '%s' does not support field selection", t)
}

View File

@@ -14,9 +14,12 @@
package checker
import "github.com/google/cel-go/checker/decls"
type options struct {
crossTypeNumericComparisons bool
homogeneousAggregateLiterals bool
validatedDeclarations *decls.Scopes
}
// Option is a functional option for configuring the type-checker
@@ -39,3 +42,12 @@ func HomogeneousAggregateLiterals(enabled bool) Option {
return nil
}
}
// ValidatedDeclarations provides a references to validated declarations which will be copied
// into new checker instances.
func ValidatedDeclarations(env *Env) Option {
return func(opts *options) error {
opts.validatedDeclarations = env.validatedDeclarations()
return nil
}
}

View File

@@ -149,21 +149,6 @@ func isEqualOrLessSpecific(t1 *exprpb.Type, t2 *exprpb.Type) bool {
}
}
return true
case kindFunction:
fn1 := t1.GetFunction()
fn2 := t2.GetFunction()
if len(fn1.ArgTypes) != len(fn2.ArgTypes) {
return false
}
if !isEqualOrLessSpecific(fn1.ResultType, fn2.ResultType) {
return false
}
for i, a1 := range fn1.ArgTypes {
if !isEqualOrLessSpecific(a1, fn2.ArgTypes[i]) {
return false
}
}
return true
case kindList:
return isEqualOrLessSpecific(t1.GetListType().ElemType, t2.GetListType().ElemType)
case kindMap:
@@ -180,43 +165,26 @@ func isEqualOrLessSpecific(t1 *exprpb.Type, t2 *exprpb.Type) bool {
/// internalIsAssignable returns true if t1 is assignable to t2.
func internalIsAssignable(m *mapping, t1 *exprpb.Type, t2 *exprpb.Type) bool {
// A type is always assignable to itself.
// Early terminate the call to avoid cases of infinite recursion.
if proto.Equal(t1, t2) {
return true
}
// Process type parameters.
kind1, kind2 := kindOf(t1), kindOf(t2)
if kind2 == kindTypeParam {
if t2Sub, found := m.find(t2); found {
// If the types are compatible, pick the more general type and return true
if !internalIsAssignable(m, t1, t2Sub) {
return false
}
m.add(t2, mostGeneral(t1, t2Sub))
// If t2 is a valid type substitution for t1, return true.
valid, t2HasSub := isValidTypeSubstitution(m, t1, t2)
if valid {
return true
}
if notReferencedIn(m, t2, t1) {
m.add(t2, t1)
return true
// If t2 is not a valid type sub for t1, and already has a known substitution return false
// since it is not possible for t1 to be a substitution for t2.
if !valid && t2HasSub {
return false
}
// Otherwise, fall through to check whether t1 is a possible substitution for t2.
}
if kind1 == kindTypeParam {
// For the lower type bound, we currently do not perform adjustment. The restricted
// way we use type parameters in lower type bounds, it is not necessary, but may
// become if we generalize type unification.
if t1Sub, found := m.find(t1); found {
// If the types are compatible, pick the more general type and return true
if !internalIsAssignable(m, t1Sub, t2) {
return false
}
m.add(t1, mostGeneral(t1Sub, t2))
return true
}
if notReferencedIn(m, t1, t2) {
m.add(t1, t2)
return true
}
// Return whether t1 is a valid substitution for t2. If not, do no additional checks as the
// possible type substitutions have been searched in both directions.
valid, _ := isValidTypeSubstitution(m, t2, t1)
return valid
}
// Next check for wildcard types.
@@ -262,18 +230,40 @@ func internalIsAssignable(m *mapping, t1 *exprpb.Type, t2 *exprpb.Type) bool {
}
}
// isValidTypeSubstitution returns whether t2 (or its type substitution) is a valid type
// substitution for t1, and whether t2 has a type substitution in mapping m.
//
// The type t2 is a valid substitution for t1 if any of the following statements is true
// - t2 has a type substitition (t2sub) equal to t1
// - t2 has a type substitution (t2sub) assignable to t1
// - t2 does not occur within t1.
func isValidTypeSubstitution(m *mapping, t1, t2 *exprpb.Type) (valid, hasSub bool) {
if t2Sub, found := m.find(t2); found {
kind1, kind2 := kindOf(t1), kindOf(t2)
if kind1 == kind2 && proto.Equal(t1, t2Sub) {
return true, true
}
// If the types are compatible, pick the more general type and return true
if internalIsAssignable(m, t1, t2Sub) {
m.add(t2, mostGeneral(t1, t2Sub))
return true, true
}
return false, true
}
if notReferencedIn(m, t2, t1) {
m.add(t2, t1)
return true, false
}
return false, false
}
// internalIsAssignableAbstractType returns true if the abstract type names agree and all type
// parameters are assignable.
func internalIsAssignableAbstractType(m *mapping,
a1 *exprpb.Type_AbstractType,
a2 *exprpb.Type_AbstractType) bool {
if a1.GetName() != a2.GetName() {
return false
}
if internalIsAssignableList(m, a1.GetParameterTypes(), a2.GetParameterTypes()) {
return true
}
return false
return a1.GetName() == a2.GetName() &&
internalIsAssignableList(m, a1.GetParameterTypes(), a2.GetParameterTypes())
}
// internalIsAssignableFunction returns true if the function return type and arg types are
@@ -421,15 +411,6 @@ func notReferencedIn(m *mapping, t *exprpb.Type, withinType *exprpb.Type) bool {
}
}
return true
case kindFunction:
fn := withinType.GetFunction()
types := flattenFunctionTypes(fn)
for _, a := range types {
if !notReferencedIn(m, t, a) {
return false
}
}
return true
case kindList:
return notReferencedIn(m, t, withinType.GetListType().ElemType)
case kindMap:
@@ -454,7 +435,6 @@ func substitute(m *mapping, t *exprpb.Type, typeParamToDyn bool) *exprpb.Type {
}
switch kind {
case kindAbstract:
// TODO: implement!
at := t.GetAbstractType()
params := make([]*exprpb.Type, len(at.GetParameterTypes()))
for i, p := range at.GetParameterTypes() {

View File

@@ -27,7 +27,7 @@ type SourceLocation struct {
}
var (
// Location implements the SourcceLocation interface.
// Location implements the SourceLocation interface.
_ Location = &SourceLocation{}
// NoLocation is a particular illegal location.
NoLocation = &SourceLocation{-1, -1}

View File

@@ -14,7 +14,7 @@
// Package operators defines the internal function names of operators.
//
// ALl operators in the expression language are modelled as function calls.
// All operators in the expression language are modelled as function calls.
package operators
// String "names" for CEL operators.

View File

@@ -73,6 +73,7 @@ go_test(
"timestamp_test.go",
"type_test.go",
"uint_test.go",
"util_test.go",
],
embed = [":go_default_library"],
deps = [

View File

@@ -130,12 +130,11 @@ func (b Bool) Value() interface{} {
}
// IsBool returns whether the input ref.Val or ref.Type is equal to BoolType.
func IsBool(elem interface{}) bool {
switch elem := elem.(type) {
case ref.Type:
return elem == BoolType
case ref.Val:
return IsBool(elem.Type())
func IsBool(elem ref.Val) bool {
switch elem.(type) {
case Bool:
return true
default:
return false
}
return false
}

View File

@@ -53,7 +53,7 @@ func (b Bytes) Add(other ref.Val) ref.Val {
return append(b, otherBytes...)
}
// Compare implments traits.Comparer interface method by lexicographic ordering.
// Compare implements traits.Comparer interface method by lexicographic ordering.
func (b Bytes) Compare(other ref.Val) ref.Val {
otherBytes, ok := other.(Bytes)
if !ok {

View File

@@ -16,6 +16,8 @@ package types
import (
"math"
"github.com/google/cel-go/common/types/ref"
)
func compareDoubleInt(d Double, i Int) Int {
@@ -74,7 +76,7 @@ func compareDouble(a, b Double) Int {
return IntZero
}
func compareInt(a, b Int) Int {
func compareInt(a, b Int) ref.Val {
if a < b {
return IntNegOne
}
@@ -84,7 +86,7 @@ func compareInt(a, b Int) Int {
return IntZero
}
func compareUint(a, b Uint) Int {
func compareUint(a, b Uint) ref.Val {
if a < b {
return IntNegOne
}

View File

@@ -78,12 +78,10 @@ func ValOrErr(val ref.Val, format string, args ...interface{}) ref.Val {
if val == nil {
return NewErr(format, args...)
}
switch val.Type() {
case ErrType, UnknownType:
if IsUnknownOrError(val) {
return val
default:
return NewErr(format, args...)
}
return NewErr(format, args...)
}
// wrapErr wraps an existing Go error value into a CEL Err value.
@@ -126,5 +124,10 @@ func (e *Err) Value() interface{} {
// IsError returns whether the input element ref.Type or ref.Val is equal to
// the ErrType singleton.
func IsError(val ref.Val) bool {
return val.Type() == ErrType
switch val.(type) {
case *Err:
return true
default:
return false
}
}

View File

@@ -96,14 +96,16 @@ func NewJSONList(adapter ref.TypeAdapter, l *structpb.ListValue) traits.Lister {
}
// NewMutableList creates a new mutable list whose internal state can be modified.
//
// The mutable list only handles `Add` calls correctly as it is intended only for use within
// comprehension loops which generate an immutable result upon completion.
func NewMutableList(adapter ref.TypeAdapter) traits.Lister {
func NewMutableList(adapter ref.TypeAdapter) traits.MutableLister {
var mutableValues []ref.Val
return &mutableList{
TypeAdapter: adapter,
baseList: nil,
mutableValues: []ref.Val{},
baseList: &baseList{
TypeAdapter: adapter,
value: mutableValues,
size: 0,
get: func(i int) interface{} { return mutableValues[i] },
},
mutableValues: mutableValues,
}
}
@@ -238,16 +240,14 @@ func (l *baseList) Equal(other ref.Val) ref.Val {
// Get implements the traits.Indexer interface method.
func (l *baseList) Get(index ref.Val) ref.Val {
i, ok := index.(Int)
if !ok {
return ValOrErr(index, "unsupported index type '%s' in list", index.Type())
ind, err := indexOrError(index)
if err != nil {
return ValOrErr(index, err.Error())
}
iv := int(i)
if iv < 0 || iv >= l.size {
return NewErr("index '%d' out of range in list size '%d'", i, l.Size())
if ind < 0 || ind >= l.size {
return NewErr("index '%d' out of range in list size '%d'", ind, l.Size())
}
elem := l.get(iv)
return l.NativeToValue(elem)
return l.NativeToValue(l.get(ind))
}
// Iterator implements the traits.Iterable interface method.
@@ -272,20 +272,25 @@ func (l *baseList) Value() interface{} {
// mutableList aggregates values into its internal storage. For use with internal CEL variables only.
type mutableList struct {
ref.TypeAdapter
*baseList
mutableValues []ref.Val
}
// Add copies elements from the other list into the internal storage of the mutable list.
// The ref.Val returned by Add is the receiver.
func (l *mutableList) Add(other ref.Val) ref.Val {
otherList, ok := other.(traits.Lister)
if !ok {
switch otherList := other.(type) {
case *mutableList:
l.mutableValues = append(l.mutableValues, otherList.mutableValues...)
l.size += len(otherList.mutableValues)
case traits.Lister:
for i := IntZero; i < otherList.Size().(Int); i++ {
l.size++
l.mutableValues = append(l.mutableValues, otherList.Get(i))
}
default:
return MaybeNoSuchOverloadErr(otherList)
}
for i := IntZero; i < otherList.Size().(Int); i++ {
l.mutableValues = append(l.mutableValues, otherList.Get(i))
}
return l
}
@@ -323,7 +328,7 @@ func (l *concatList) Add(other ref.Val) ref.Val {
nextList: otherList}
}
// Contains implments the traits.Container interface method.
// Contains implements the traits.Container interface method.
func (l *concatList) Contains(elem ref.Val) ref.Val {
// The concat list relies on the IsErrorOrUnknown checks against the input element to be
// performed by the `prevList` and/or `nextList`.
@@ -391,10 +396,11 @@ func (l *concatList) Equal(other ref.Val) ref.Val {
// Get implements the traits.Indexer interface method.
func (l *concatList) Get(index ref.Val) ref.Val {
i, ok := index.(Int)
if !ok {
return MaybeNoSuchOverloadErr(index)
ind, err := indexOrError(index)
if err != nil {
return ValOrErr(index, err.Error())
}
i := Int(ind)
if i < l.prevList.Size().(Int) {
return l.prevList.Get(i)
}
@@ -462,3 +468,22 @@ func (it *listIterator) Next() ref.Val {
}
return nil
}
func indexOrError(index ref.Val) (int, error) {
switch iv := index.(type) {
case Int:
return int(iv), nil
case Double:
if ik, ok := doubleToInt64Lossless(float64(iv)); ok {
return int(ik), nil
}
return -1, fmt.Errorf("unsupported index value %v in list", index)
case Uint:
if ik, ok := uint64ToInt64Lossless(uint64(iv)); ok {
return int(ik), nil
}
return -1, fmt.Errorf("unsupported index value %v in list", index)
default:
return -1, fmt.Errorf("unsupported index type '%s' in list", index.Type())
}
}

View File

@@ -105,7 +105,7 @@ var (
// This interface implements portions of the API surface area required by the traits.Mapper
// interface.
type mapAccessor interface {
// Find returns a value, if one exists, for the inpput key.
// Find returns a value, if one exists, for the input key.
//
// If the key is not found the function returns (nil, false).
Find(ref.Val) (ref.Val, bool)
@@ -429,7 +429,7 @@ func (a *refValMapAccessor) Find(key ref.Val) (ref.Val, bool) {
case Double:
if ik, ok := doubleToInt64Lossless(float64(k)); ok {
if keyVal, found := a.mapVal[Int(ik)]; found {
return keyVal, true
return keyVal, found
}
}
if uk, ok := doubleToUint64Lossless(float64(k)); ok {

View File

@@ -316,7 +316,7 @@ func doubleToUint64Checked(v float64) (uint64, error) {
return uint64(v), nil
}
// int64toUint64Checked converts an int64 to a uint64 value.
// int64ToUint64Checked converts an int64 to a uint64 value.
//
// If the conversion fails due to overflow the error return value will be non-nil.
func int64ToUint64Checked(v int64) (uint64, error) {
@@ -326,7 +326,7 @@ func int64ToUint64Checked(v int64) (uint64, error) {
return uint64(v), nil
}
// int64toInt32Checked converts an int64 to an int32 value.
// int64ToInt32Checked converts an int64 to an int32 value.
//
// If the conversion fails due to overflow the error return value will be non-nil.
func int64ToInt32Checked(v int64) (int32, error) {
@@ -336,7 +336,7 @@ func int64ToInt32Checked(v int64) (int32, error) {
return int32(v), nil
}
// uint64toUint32Checked converts a uint64 to a uint32 value.
// uint64ToUint32Checked converts a uint64 to a uint32 value.
//
// If the conversion fails due to overflow the error return value will be non-nil.
func uint64ToUint32Checked(v uint64) (uint32, error) {
@@ -346,7 +346,7 @@ func uint64ToUint32Checked(v uint64) (uint32, error) {
return uint32(v), nil
}
// uint64toInt64Checked converts a uint64 to an int64 value.
// uint64ToInt64Checked converts a uint64 to an int64 value.
//
// If the conversion fails due to overflow the error return value will be non-nil.
func uint64ToInt64Checked(v uint64) (int64, error) {

View File

@@ -167,7 +167,7 @@ func (s String) Match(pattern ref.Val) ref.Val {
return Bool(matched)
}
// Receive implements traits.Reciever.Receive.
// Receive implements traits.Receiver.Receive.
func (s String) Receive(function string, overload string, args []ref.Val) ref.Val {
switch len(args) {
case 1:

View File

@@ -138,7 +138,7 @@ func (t Timestamp) Equal(other ref.Val) ref.Val {
return Bool(ok && t.Time.Equal(otherTime.Time))
}
// Receive implements traits.Reciever.Receive.
// Receive implements traits.Receiver.Receive.
func (t Timestamp) Receive(function string, overload string, args []ref.Val) ref.Val {
switch len(args) {
case 0:

View File

@@ -28,5 +28,6 @@ type Lister interface {
// MutableLister interface which emits an immutable result after an intermediate computation.
type MutableLister interface {
Lister
ToImmutableList() Lister
}

View File

@@ -34,12 +34,12 @@ func (u Unknown) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return u.Value(), nil
}
// ConvertToType implements ref.Val.ConvertToType.
// ConvertToType is an identity function since unknown values cannot be modified.
func (u Unknown) ConvertToType(typeVal ref.Type) ref.Val {
return u
}
// Equal implements ref.Val.Equal.
// Equal is an identity function since unknown values cannot be modified.
func (u Unknown) Equal(other ref.Val) ref.Val {
return u
}
@@ -57,5 +57,10 @@ func (u Unknown) Value() interface{} {
// IsUnknown returns whether the element ref.Type or ref.Val is equal to the
// UnknownType singleton.
func IsUnknown(val ref.Val) bool {
return val.Type() == UnknownType
switch val.(type) {
case Unknown:
return true
default:
return false
}
}

View File

@@ -18,10 +18,10 @@ import (
"github.com/google/cel-go/common/types/ref"
)
// IsUnknownOrError returns whether the input element ref.Val is an ErrType or UnknonwType.
// IsUnknownOrError returns whether the input element ref.Val is an ErrType or UnknownType.
func IsUnknownOrError(val ref.Val) bool {
switch val.Type() {
case UnknownType, ErrType:
switch val.(type) {
case Unknown, *Err:
return true
}
return false

View File

@@ -35,13 +35,17 @@ type Activation interface {
Parent() Activation
}
// EmptyActivation returns a variable free activation.
// EmptyActivation returns a variable-free activation.
func EmptyActivation() Activation {
// This call cannot fail.
a, _ := NewActivation(map[string]interface{}{})
return a
return emptyActivation{}
}
// emptyActivation is a variable-free activation.
type emptyActivation struct{}
func (emptyActivation) ResolveName(string) (interface{}, 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.
//

View File

@@ -105,7 +105,7 @@ func (apat *AttributePattern) QualifierPatterns() []*AttributeQualifierPattern {
return apat.qualifierPatterns
}
// AttributeQualifierPattern holds a wilcard or valued qualifier pattern.
// AttributeQualifierPattern holds a wildcard or valued qualifier pattern.
type AttributeQualifierPattern struct {
wildcard bool
value interface{}
@@ -125,7 +125,7 @@ func (qpat *AttributeQualifierPattern) Matches(q Qualifier) bool {
// 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, Attriutes are resolved before
// 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.
//
@@ -206,7 +206,7 @@ func (fac *partialAttributeFactory) AbsoluteAttribute(id int64, names ...string)
}
// MaybeAttribute implementation of the AttributeFactory interface which ensure that the set of
// 'maybe' NamespacedAttribute values are produced using the PartialAttributeFactory rather than
// '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{

View File

@@ -587,7 +587,7 @@ func (a *relativeAttribute) Resolve(vars Activation) (interface{}, error) {
if types.IsUnknown(v) {
return v, nil
}
// Next, qualify it. Qualification handles unkonwns as well, so there's no need to recheck.
// Next, qualify it. Qualification handles unknowns as well, so there's no need to recheck.
var err error
var obj interface{} = v
for _, qual := range a.qualifiers {
@@ -637,6 +637,8 @@ func newQualifier(adapter ref.TypeAdapter, id int64, v interface{}) (Qualifier,
qual = &uintQualifier{id: id, value: uint64(val), celValue: val, adapter: adapter}
case types.Bool:
qual = &boolQualifier{id: id, value: bool(val), celValue: val, adapter: adapter}
case types.Double:
qual = &doubleQualifier{id: id, value: float64(val), celValue: val, adapter: adapter}
default:
return nil, fmt.Errorf("invalid qualifier type: %T", v)
}

View File

@@ -98,7 +98,7 @@ func decDisableShortcircuits() InterpretableDecorator {
}
// decOptimize optimizes the program plan by looking for common evaluation patterns and
// conditionally precomputating the result.
// conditionally precomputing the result.
// - build list and map values with constant elements.
// - convert 'in' operations to set membership tests if possible.
func decOptimize() InterpretableDecorator {

View File

@@ -677,7 +677,19 @@ func (m *evalMap) Eval(ctx Activation) ref.Val {
}
func (m *evalMap) InitVals() []Interpretable {
return append(m.keys, m.vals...)
if len(m.keys) != len(m.vals) {
return nil
}
result := make([]Interpretable, len(m.keys)+len(m.vals))
idx := 0
for i, k := range m.keys {
v := m.vals[i]
result[idx] = k
idx++
result[idx] = v
idx++
}
return result
}
func (m *evalMap) Type() ref.Type {
@@ -852,7 +864,7 @@ func (fold *evalFold) Cost() (min, max int64) {
iMax + aMax + cMax*rangeCnt + sMax*rangeCnt + rMax
}
// Optional Intepretable implementations that specialize, subsume, or extend the core evaluation
// Optional Interpretable implementations that specialize, subsume, or extend the core evaluation
// plan via decorators.
// evalSetMembership is an Interpretable implementation which tests whether an input value
@@ -969,7 +981,7 @@ func (e *evalWatchConstQual) Qualify(vars Activation, obj interface{}) (interfac
return out, err
}
// QualifierValueEquals tests whether the incoming value is equal to the qualificying constant.
// QualifierValueEquals tests whether the incoming value is equal to the qualifying constant.
func (e *evalWatchConstQual) QualifierValueEquals(value interface{}) bool {
qve, ok := e.ConstantQualifier.(qualifierValueEquator)
return ok && qve.QualifierValueEquals(value)

View File

@@ -162,7 +162,7 @@ type exprInterpreter struct {
}
// NewInterpreter builds an Interpreter from a Dispatcher and TypeProvider which will be used
// throughout the Eval of all Interpretable instances gerenated from it.
// throughout the Eval of all Interpretable instances generated from it.
func NewInterpreter(dispatcher Dispatcher,
container *containers.Container,
provider ref.TypeProvider,

View File

@@ -77,7 +77,7 @@ func newUncheckedPlanner(disp Dispatcher,
}
}
// planner is an implementatio of the interpretablePlanner interface.
// planner is an implementation of the interpretablePlanner interface.
type planner struct {
disp Dispatcher
provider ref.TypeProvider

View File

@@ -228,7 +228,7 @@ func (p *astPruner) prune(node *exprpb.Expr) (*exprpb.Expr, bool) {
}
}
// We have either an unknown/error value, or something we dont want to
// 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.

View File

@@ -41,26 +41,42 @@ func CostObserver(tracker *CostTracker) EvalObserver {
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.stack.pop(1)
tracker.cost++
case InterpretableConst:
// zero cost
case InterpretableAttribute:
// Ternary has no direct cost. All cost is from the conditional and the true/false branch expressions.
_, isConditional := t.Attr().(*conditionalAttribute)
if !isConditional {
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
}
case *evalExhaustiveConditional, *evalOr, *evalAnd, *evalExhaustiveOr, *evalExhaustiveAnd:
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:
tracker.stack.drop(t.rhs.ID(), t.lhs.ID())
case *evalAnd:
tracker.stack.drop(t.rhs.ID(), t.lhs.ID())
case *evalExhaustiveOr:
tracker.stack.drop(t.rhs.ID(), t.lhs.ID())
case *evalExhaustiveAnd:
tracker.stack.drop(t.rhs.ID(), t.lhs.ID())
case *evalFold:
tracker.stack.drop(t.iterRange.ID())
case Qualifier:
tracker.stack.pop(1)
tracker.cost++
case InterpretableCall:
if argVals, ok := tracker.stack.pop(len(t.Args())); ok {
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
@@ -70,7 +86,7 @@ func CostObserver(tracker *CostTracker) EvalObserver {
tracker.cost += common.StructCreateBaseCost
}
}
tracker.stack.push(val)
tracker.stack.push(val, id)
if tracker.Limit != nil && tracker.cost > *tracker.Limit {
panic(EvalCancelledError{Cause: CostLimitExceeded, Message: "operation cancelled: actual cost limit exceeded"})
@@ -170,19 +186,56 @@ func (c CostTracker) actualSize(value ref.Val) uint64 {
return 1
}
// refValStack keeps track of values of the stack for cost calculation purposes
type refValStack []ref.Val
type stackVal struct {
Val ref.Val
ID int64
}
func (s *refValStack) push(value ref.Val) {
// 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)
}
func (s *refValStack) pop(count int) ([]ref.Val, bool) {
if len(*s) < count {
// 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
}
idx := len(*s) - count
el := (*s)[idx:]
*s = (*s)[:idx]
return el, true
return result, true
}