mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-19 04:06:35 +00:00
🌱 Bump github.com/google/cel-go from 0.28.1 to 0.29.0 (#1613)
Bumps [github.com/google/cel-go](https://github.com/google/cel-go) from 0.28.1 to 0.29.0. - [Release notes](https://github.com/google/cel-go/releases) - [Commits](https://github.com/google/cel-go/compare/v0.28.1...v0.29.0) --- updated-dependencies: - dependency-name: github.com/google/cel-go dependency-version: 0.29.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/google/cel-go v0.28.1
|
||||
github.com/google/cel-go v0.29.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/itchyny/gojq v0.12.19
|
||||
github.com/onsi/ginkgo/v2 v2.32.0
|
||||
|
||||
@@ -220,8 +220,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
|
||||
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
github.com/google/cel-go v0.29.0 h1:fEG+Ja3YRwNOqnQxTyJwoByAUAvTuxUGiro/jhrm4F4=
|
||||
github.com/google/cel-go v0.29.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
|
||||
+18
-2
@@ -436,6 +436,15 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
|
||||
return ast, nil
|
||||
}
|
||||
|
||||
// configuredExpressionSizeLimit returns the effective expression size code point limit.
|
||||
// A zero value means "use the parser default".
|
||||
func (e *Env) configuredExpressionSizeLimit() int {
|
||||
if l := e.limits[limitCodePointSize]; l != 0 {
|
||||
return l
|
||||
}
|
||||
return 100_000
|
||||
}
|
||||
|
||||
// Compile combines the Parse and Check phases CEL program compilation to produce an Ast and
|
||||
// associated issues.
|
||||
//
|
||||
@@ -445,7 +454,11 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
|
||||
//
|
||||
// Note, for parse-only uses of CEL use Parse.
|
||||
func (e *Env) Compile(txt string) (*Ast, *Issues) {
|
||||
return e.CompileSource(common.NewTextSource(txt))
|
||||
src, err := common.NewTextSourceWithLimit(txt, e.configuredExpressionSizeLimit())
|
||||
if err != nil {
|
||||
return nil, ErrorAsIssues(err)
|
||||
}
|
||||
return e.CompileSource(src)
|
||||
}
|
||||
|
||||
// CompileSource combines the Parse and Check phases CEL program compilation to produce an Ast and
|
||||
@@ -650,7 +663,10 @@ func (e *Env) Validators() []ASTValidator {
|
||||
// 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)
|
||||
src, err := common.NewTextSourceWithLimit(txt, e.configuredExpressionSizeLimit())
|
||||
if err != nil {
|
||||
return nil, ErrorAsIssues(err)
|
||||
}
|
||||
return e.ParseSource(src)
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -97,7 +97,7 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST)
|
||||
continue
|
||||
}
|
||||
// Late-bound function calls cannot be folded.
|
||||
if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, a, fold) {
|
||||
if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, fold) {
|
||||
continue
|
||||
}
|
||||
// Otherwise, assume all context is needed to evaluate the expression.
|
||||
@@ -168,7 +168,7 @@ func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST,
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLateBoundFunctionCall(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) bool {
|
||||
func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool {
|
||||
call := expr.AsCall()
|
||||
function := ctx.Functions()[call.FunctionName()]
|
||||
if function == nil {
|
||||
@@ -518,7 +518,7 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext,
|
||||
constantExprs = false
|
||||
}
|
||||
// Late-bound function calls cannot be folded.
|
||||
if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, a, e) {
|
||||
if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, e) {
|
||||
constantExprs = false
|
||||
}
|
||||
})
|
||||
|
||||
+24
-16
@@ -590,7 +590,7 @@ func (lib *optionalLib) CompileOptions() []EnvOption {
|
||||
// ProgramOptions implements the Library interface method.
|
||||
func (lib *optionalLib) ProgramOptions() []ProgramOption {
|
||||
return []ProgramOption{
|
||||
CustomDecorator(decorateOptionalOr),
|
||||
CustomDecoratorV2(decorateOptionalOr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,7 +683,7 @@ func EnableErrorOnBadPresenceTest(value bool) EnvOption {
|
||||
return features(featureEnableErrorOnBadPresenceTest, value)
|
||||
}
|
||||
|
||||
func decorateOptionalOr(i interpreter.Interpretable) (interpreter.Interpretable, error) {
|
||||
func decorateOptionalOr(i interpreter.InterpretableV2) (interpreter.InterpretableV2, error) {
|
||||
call, ok := i.(interpreter.InterpretableCall)
|
||||
if !ok {
|
||||
return i, nil
|
||||
@@ -720,8 +720,8 @@ func decorateOptionalOr(i interpreter.Interpretable) (interpreter.Interpretable,
|
||||
// the second optional expression is evaluated and returned.
|
||||
type evalOptionalOr struct {
|
||||
id int64
|
||||
lhs interpreter.Interpretable
|
||||
rhs interpreter.Interpretable
|
||||
lhs interpreter.InterpretableV2
|
||||
rhs interpreter.InterpretableV2
|
||||
}
|
||||
|
||||
// ID implements the Interpretable interface method.
|
||||
@@ -729,11 +729,9 @@ func (opt *evalOptionalOr) ID() int64 {
|
||||
return opt.id
|
||||
}
|
||||
|
||||
// Eval evaluates the left-hand side optional to determine whether it contains a value, else
|
||||
// proceeds with the right-hand side evaluation.
|
||||
func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val {
|
||||
func (opt *evalOptionalOr) Exec(frame *interpreter.ExecutionFrame) ref.Val {
|
||||
// short-circuit lhs.
|
||||
optLHS := opt.lhs.Eval(ctx)
|
||||
optLHS := opt.lhs.Exec(frame)
|
||||
switch val := optLHS.(type) {
|
||||
case *types.Err, *types.Unknown:
|
||||
return optLHS
|
||||
@@ -741,18 +739,24 @@ func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val {
|
||||
if val.HasValue() {
|
||||
return optLHS
|
||||
}
|
||||
return opt.rhs.Eval(ctx)
|
||||
return opt.rhs.Exec(frame)
|
||||
default:
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Eval evaluates the left-hand side optional to determine whether it contains a value, else
|
||||
// proceeds with the right-hand side evaluation.
|
||||
func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val {
|
||||
return opt.Exec(interpreter.AsFrame(ctx))
|
||||
}
|
||||
|
||||
// evalOptionalOrValue selects between an optional or a concrete value. If the optional has a value,
|
||||
// its value is returned, otherwise the alternative value expression is evaluated and returned.
|
||||
type evalOptionalOrValue struct {
|
||||
id int64
|
||||
lhs interpreter.Interpretable
|
||||
rhs interpreter.Interpretable
|
||||
lhs interpreter.InterpretableV2
|
||||
rhs interpreter.InterpretableV2
|
||||
}
|
||||
|
||||
// ID implements the Interpretable interface method.
|
||||
@@ -760,11 +764,9 @@ func (opt *evalOptionalOrValue) ID() int64 {
|
||||
return opt.id
|
||||
}
|
||||
|
||||
// Eval evaluates the left-hand side optional to determine whether it contains a value, else
|
||||
// proceeds with the right-hand side evaluation.
|
||||
func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val {
|
||||
func (opt *evalOptionalOrValue) Exec(frame *interpreter.ExecutionFrame) ref.Val {
|
||||
// short-circuit lhs.
|
||||
optLHS := opt.lhs.Eval(ctx)
|
||||
optLHS := opt.lhs.Exec(frame)
|
||||
|
||||
switch val := optLHS.(type) {
|
||||
case *types.Err, *types.Unknown:
|
||||
@@ -773,12 +775,18 @@ func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val {
|
||||
if val.HasValue() {
|
||||
return val.GetValue()
|
||||
}
|
||||
return opt.rhs.Eval(ctx)
|
||||
return opt.rhs.Exec(frame)
|
||||
default:
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Eval evaluates the left-hand side optional to determine whether it contains a value, else
|
||||
// proceeds with the right-hand side evaluation.
|
||||
func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val {
|
||||
return opt.Exec(interpreter.AsFrame(ctx))
|
||||
}
|
||||
|
||||
type timeLegacyLibrary struct{}
|
||||
|
||||
func (timeLegacyLibrary) CompileOptions() []EnvOption {
|
||||
|
||||
+8
@@ -456,6 +456,14 @@ func CustomDecorator(dec interpreter.InterpretableDecorator) ProgramOption {
|
||||
}
|
||||
}
|
||||
|
||||
// CustomDecoratorV2 appends an InterpreterDecoratorV2 to the program.
|
||||
func CustomDecoratorV2(dec interpreter.InterpretableDecoratorV2) ProgramOption {
|
||||
return func(p *prog) (*prog, error) {
|
||||
p.plannerOptions = append(p.plannerOptions, interpreter.CustomDecoratorV2(dec))
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Functions adds function overloads that extend or override the set of CEL built-ins.
|
||||
//
|
||||
// Deprecated: use Function() instead to declare the function, its overload signatures,
|
||||
|
||||
+40
-171
@@ -18,7 +18,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/common/ast"
|
||||
"github.com/google/cel-go/common/functions"
|
||||
@@ -160,7 +159,7 @@ type prog struct {
|
||||
regexOptimizations []*interpreter.RegexOptimization
|
||||
|
||||
// Interpretable configured from an Ast and aggregate decorator set based on program options.
|
||||
interpretable interpreter.Interpretable
|
||||
interpretable interpreter.InterpretableV2
|
||||
observable *interpreter.ObservableInterpretable
|
||||
callCostEstimator interpreter.ActualCostEstimator
|
||||
costOptions []interpreter.CostTrackerOption
|
||||
@@ -262,8 +261,16 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) {
|
||||
if p.costLimit != nil {
|
||||
costOpts = append(costOpts, interpreter.CostTrackerLimit(*p.costLimit))
|
||||
}
|
||||
// Creating a new cost tracker for each evaluation causes significant work that
|
||||
// needs to be repeated for each evaluation even though the cost tracker is
|
||||
// mostly read-only once constructed. Therefore it gets constructed
|
||||
// once now and later a cheap clone is used for each evaluation.
|
||||
tracker, err := interpreter.NewCostTracker(p.callCostEstimator, costOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("construct cost tracker: %w", err)
|
||||
}
|
||||
trackerFactory := func() (*interpreter.CostTracker, error) {
|
||||
return interpreter.NewCostTracker(p.callCostEstimator, costOpts...)
|
||||
return tracker.Clone()
|
||||
}
|
||||
var observers []interpreter.PlannerOption
|
||||
if p.evalOpts&(OptExhaustiveEval|OptTrackState) != 0 {
|
||||
@@ -313,22 +320,19 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) {
|
||||
}
|
||||
}()
|
||||
// Build a hierarchical activation if there are default vars set.
|
||||
var vars Activation
|
||||
switch v := input.(type) {
|
||||
case Activation:
|
||||
vars = v
|
||||
case map[string]any:
|
||||
vars = activationPool.Setup(v)
|
||||
defer activationPool.Put(vars)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input)
|
||||
}
|
||||
if p.defaultVars != nil {
|
||||
vars = interpreter.NewHierarchicalActivation(p.defaultVars, vars)
|
||||
var frame *interpreter.ExecutionFrame
|
||||
if f, ok := input.(*interpreter.ExecutionFrame); ok {
|
||||
frame = f
|
||||
} else {
|
||||
frame, err = p.newExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer frame.Close()
|
||||
}
|
||||
if p.observable != nil {
|
||||
det = &EvalDetails{}
|
||||
out = p.observable.ObserveEval(vars, func(observed any) {
|
||||
out = p.observable.ObserveExec(frame, func(observed any) {
|
||||
switch o := observed.(type) {
|
||||
case interpreter.EvalState:
|
||||
det.state = o
|
||||
@@ -337,7 +341,7 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
out = p.interpretable.Eval(vars)
|
||||
out = p.interpretable.Exec(frame)
|
||||
}
|
||||
// The output of an internal Eval may have a value (`v`) that is a types.Err. This step
|
||||
// translates the CEL value to a Go error response. This interface does not quite match the
|
||||
@@ -353,164 +357,29 @@ func (p *prog) ContextEval(ctx context.Context, input any) (ref.Val, *EvalDetail
|
||||
if ctx == nil {
|
||||
return nil, nil, fmt.Errorf("context can not be nil")
|
||||
}
|
||||
// Configure the input, making sure to wrap Activation inputs in the special ctxActivation which
|
||||
// exposes the #interrupted variable and manages rate-limited checks of the ctx.Done() state.
|
||||
var vars Activation
|
||||
switch v := input.(type) {
|
||||
case Activation:
|
||||
vars = ctxActivationPool.Setup(v, ctx.Done(), p.interruptCheckFrequency)
|
||||
defer ctxActivationPool.Put(vars)
|
||||
case map[string]any:
|
||||
rawVars := activationPool.Setup(v)
|
||||
defer activationPool.Put(rawVars)
|
||||
vars = ctxActivationPool.Setup(rawVars, ctx.Done(), p.interruptCheckFrequency)
|
||||
defer ctxActivationPool.Put(vars)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input)
|
||||
frame, err := p.newExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
out, det, err := p.Eval(vars)
|
||||
if err != nil && errors.Is(err, interpreter.InterruptError{}) {
|
||||
return out, det, fmt.Errorf("%w: %w", err, context.Cause(ctx))
|
||||
defer frame.Close()
|
||||
frame.SetContext(ctx, p.interruptCheckFrequency)
|
||||
out, det, errEval := p.Eval(frame)
|
||||
if errEval != nil && errors.Is(errEval, interpreter.InterruptError{}) {
|
||||
return out, det, fmt.Errorf("%w: %w", errEval, context.Cause(ctx))
|
||||
}
|
||||
return out, det, err
|
||||
return out, det, errEval
|
||||
}
|
||||
|
||||
type ctxEvalActivation struct {
|
||||
parent Activation
|
||||
interrupt <-chan struct{}
|
||||
interruptCheckCount uint
|
||||
interruptCheckFrequency uint
|
||||
}
|
||||
|
||||
// ResolveName implements the Activation interface method, but adds a special #interrupted variable
|
||||
// which is capable of testing whether a 'done' signal is provided from a context.Context channel.
|
||||
func (a *ctxEvalActivation) ResolveName(name string) (any, bool) {
|
||||
if name == "#interrupted" {
|
||||
a.interruptCheckCount++
|
||||
if a.interruptCheckCount%a.interruptCheckFrequency == 0 {
|
||||
select {
|
||||
case <-a.interrupt:
|
||||
return true, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
// newExecutionFrame creates an ExecutionFrame for the given input without a timeout context.
|
||||
func (p *prog) newExecutionFrame(input any) (*interpreter.ExecutionFrame, error) {
|
||||
frame, err := interpreter.NewExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.parent.ResolveName(name)
|
||||
}
|
||||
|
||||
func (a *ctxEvalActivation) Parent() Activation {
|
||||
return a.parent
|
||||
}
|
||||
|
||||
func (a *ctxEvalActivation) AsPartialActivation() (interpreter.PartialActivation, bool) {
|
||||
pa, ok := a.parent.(interpreter.PartialActivation)
|
||||
return pa, ok
|
||||
}
|
||||
|
||||
func newCtxEvalActivationPool() *ctxEvalActivationPool {
|
||||
return &ctxEvalActivationPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() any {
|
||||
return &ctxEvalActivation{}
|
||||
},
|
||||
},
|
||||
if p.defaultVars != nil {
|
||||
// Update the frame's activation in place.
|
||||
frame.Activation = interpreter.NewHierarchicalActivation(p.defaultVars, frame.Activation)
|
||||
}
|
||||
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
type ctxEvalActivationPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
// Setup initializes a pooled Activation with the ability check for context.Context cancellation
|
||||
func (p *ctxEvalActivationPool) Setup(vars Activation, done <-chan struct{}, interruptCheckRate uint) *ctxEvalActivation {
|
||||
a := p.Pool.Get().(*ctxEvalActivation)
|
||||
a.parent = vars
|
||||
a.interrupt = done
|
||||
a.interruptCheckCount = 0
|
||||
a.interruptCheckFrequency = interruptCheckRate
|
||||
return a
|
||||
}
|
||||
|
||||
type evalActivation struct {
|
||||
vars map[string]any
|
||||
lazyVars map[string]any
|
||||
}
|
||||
|
||||
// ResolveName looks up the value of the input variable name, if found.
|
||||
//
|
||||
// Lazy bindings may be supplied within the map-based input in either of the following forms:
|
||||
// - func() any
|
||||
// - func() ref.Val
|
||||
//
|
||||
// The lazy binding will only be invoked once per evaluation.
|
||||
//
|
||||
// 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 (a *evalActivation) ResolveName(name string) (any, bool) {
|
||||
v, found := a.vars[name]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
switch obj := v.(type) {
|
||||
case func() ref.Val:
|
||||
if resolved, found := a.lazyVars[name]; found {
|
||||
return resolved, true
|
||||
}
|
||||
lazy := obj()
|
||||
a.lazyVars[name] = lazy
|
||||
return lazy, true
|
||||
case func() any:
|
||||
if resolved, found := a.lazyVars[name]; found {
|
||||
return resolved, true
|
||||
}
|
||||
lazy := obj()
|
||||
a.lazyVars[name] = lazy
|
||||
return lazy, true
|
||||
default:
|
||||
return obj, true
|
||||
}
|
||||
}
|
||||
|
||||
// Parent implements the Activation interface
|
||||
func (a *evalActivation) Parent() Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEvalActivationPool() *evalActivationPool {
|
||||
return &evalActivationPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() any {
|
||||
return &evalActivation{lazyVars: make(map[string]any)}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type evalActivationPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
// Setup initializes a pooled Activation object with the map input.
|
||||
func (p *evalActivationPool) Setup(vars map[string]any) *evalActivation {
|
||||
a := p.Pool.Get().(*evalActivation)
|
||||
a.vars = vars
|
||||
return a
|
||||
}
|
||||
|
||||
func (p *evalActivationPool) Put(value any) {
|
||||
a := value.(*evalActivation)
|
||||
for k := range a.lazyVars {
|
||||
delete(a.lazyVars, k)
|
||||
}
|
||||
p.Pool.Put(a)
|
||||
}
|
||||
|
||||
var (
|
||||
// activationPool is an internally managed pool of Activation values that wrap map[string]any inputs
|
||||
activationPool = newEvalActivationPool()
|
||||
|
||||
// ctxActivationPool is an internally managed pool of Activation values that expose a special #interrupted variable
|
||||
ctxActivationPool = newCtxEvalActivationPool()
|
||||
)
|
||||
|
||||
+233
-226
@@ -1,226 +1,233 @@
|
||||
// Copyright 2025 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
|
||||
//
|
||||
// https://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 cel
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
"github.com/google/cel-go/common/overloads"
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
//go:embed templates/authoring.tmpl
|
||||
var authoringPrompt string
|
||||
|
||||
// splitImpl splits a string into a list of strings.
|
||||
//
|
||||
// Normalizes extracted comments (trim common prefix whitespace and extra trailing newlines).
|
||||
func splitImpl(str string) []string {
|
||||
str = strings.TrimRight(str, " \n\t\r")
|
||||
out := strings.Split(str, "\n")
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
negative := strings.TrimLeft(out[0], " \t")
|
||||
lenNegative := len(negative)
|
||||
lenOut := len(out[0])
|
||||
if lenNegative == lenOut {
|
||||
return out
|
||||
}
|
||||
prefix := out[0][:lenOut-lenNegative]
|
||||
trimmed := make([]string, len(out))
|
||||
for i, line := range out {
|
||||
if line == "" {
|
||||
trimmed[i] = ""
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
return out
|
||||
}
|
||||
trimmed[i] = strings.TrimPrefix(line, prefix)
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// AuthoringPrompt creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
|
||||
func AuthoringPrompt(env *Env) (*Prompt, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"split": splitImpl,
|
||||
"newlineToSpace": func(str string) string { return strings.ReplaceAll(str, "\n", " ") },
|
||||
}
|
||||
tmpl := template.New("cel").Funcs(funcMap)
|
||||
tmpl, err := tmpl.Parse(authoringPrompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Prompt{
|
||||
Persona: defaultPersona,
|
||||
FormatRules: defaultFormatRules,
|
||||
GeneralUsage: defaultGeneralUsage,
|
||||
tmpl: tmpl,
|
||||
env: env,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthoringPromptWithFieldPaths creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
|
||||
// Includes documentation for all of the reachable field paths in the environment.
|
||||
func AuthoringPromptWithFieldPaths(env *Env) (*Prompt, error) {
|
||||
p, err := AuthoringPrompt(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.fieldPaths = true
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Prompt represents the core components of an LLM prompt based on a CEL environment.
|
||||
//
|
||||
// All fields of the prompt may be overwritten / modified with support for rendering the
|
||||
// prompt to a human-readable string.
|
||||
type Prompt struct {
|
||||
// Persona indicates something about the kind of user making the request
|
||||
Persona string
|
||||
|
||||
// FormatRules indicate how the LLM should generate its output
|
||||
FormatRules string
|
||||
|
||||
// GeneralUsage specifies additional context on how CEL should be used.
|
||||
GeneralUsage string
|
||||
|
||||
// tmpl is the text template base-configuration for rendering text.
|
||||
tmpl *template.Template
|
||||
|
||||
// fieldPaths is a flag to enable including reachable field paths in the prompt.
|
||||
fieldPaths bool
|
||||
|
||||
// env reference used to collect variables, functions, and macros available to the prompt.
|
||||
env *Env
|
||||
}
|
||||
|
||||
type promptVariable struct {
|
||||
*common.Doc
|
||||
FieldPaths []*common.Doc
|
||||
}
|
||||
|
||||
type promptInst struct {
|
||||
*Prompt
|
||||
|
||||
Variables []*promptVariable
|
||||
Macros []*common.Doc
|
||||
Functions []*common.Doc
|
||||
UserPrompt string
|
||||
}
|
||||
|
||||
// Render renders the user prompt with the associated context from the prompt template
|
||||
// for use with LLM generators.
|
||||
func (p *Prompt) Render(userPrompt string) string {
|
||||
var buffer strings.Builder
|
||||
vars := make([]*promptVariable, len(p.env.Variables()))
|
||||
for i, v := range p.env.Variables() {
|
||||
vars[i] = &promptVariable{Doc: v.Documentation()}
|
||||
if p.fieldPaths && v.Type().Kind() == types.StructKind {
|
||||
var fieldPaths []*common.Doc
|
||||
|
||||
paths := fieldPathsForType(p.env.CELTypeProvider(), v.Name(), v.Type())
|
||||
if len(paths) < 2 {
|
||||
paths = nil
|
||||
} else {
|
||||
// First path is the variable which is already documented.
|
||||
paths = paths[1:]
|
||||
}
|
||||
for _, path := range paths {
|
||||
fieldPaths = append(fieldPaths, path.Documentation())
|
||||
}
|
||||
|
||||
sort.SliceStable(fieldPaths, func(i, j int) bool {
|
||||
return fieldPaths[i].Name < fieldPaths[j].Name
|
||||
})
|
||||
vars[i].FieldPaths = fieldPaths
|
||||
}
|
||||
}
|
||||
sort.SliceStable(vars, func(i, j int) bool {
|
||||
return vars[i].Name < vars[j].Name
|
||||
})
|
||||
macs := make([]*common.Doc, len(p.env.Macros()))
|
||||
for i, m := range p.env.Macros() {
|
||||
macs[i] = m.(common.Documentor).Documentation()
|
||||
}
|
||||
funcs := make([]*common.Doc, 0, len(p.env.Functions()))
|
||||
for _, f := range p.env.Functions() {
|
||||
if _, hidden := hiddenFunctions[f.Name()]; hidden {
|
||||
continue
|
||||
}
|
||||
funcs = append(funcs, f.Documentation())
|
||||
}
|
||||
sort.SliceStable(funcs, func(i, j int) bool {
|
||||
return funcs[i].Name < funcs[j].Name
|
||||
})
|
||||
inst := &promptInst{
|
||||
Prompt: p,
|
||||
Variables: vars,
|
||||
Macros: macs,
|
||||
Functions: funcs,
|
||||
UserPrompt: userPrompt}
|
||||
p.tmpl.Execute(&buffer, inst)
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPersona = `You are a software engineer with expertise in networking and application security
|
||||
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
|
||||
networking, authentication, and data access is only permitted when all conditions
|
||||
are satisfied.`
|
||||
|
||||
defaultFormatRules = `Output your response as a CEL expression.
|
||||
|
||||
Write the expression with the comment on the first line and the expression on the
|
||||
subsequent lines. Format the expression using 80-character line limits commonly
|
||||
found in C++ or Java code.`
|
||||
|
||||
defaultGeneralUsage = `CEL supports Protocol Buffer and JSON types, as well as simple types and aggregate types.
|
||||
|
||||
Simple types include bool, bytes, double, int, string, and uint:
|
||||
|
||||
* double literals must always include a decimal point: 1.0, 3.5, -2.2
|
||||
* uint literals must be positive values suffixed with a 'u': 42u
|
||||
* byte literals are strings prefixed with a 'b': b'1235'
|
||||
* string literals can use either single quotes or double quotes: 'hello', "world"
|
||||
* string literals can also be treated as raw strings that do not require any
|
||||
escaping within the string by using the 'R' prefix: R"""quote: "hi" """
|
||||
|
||||
Aggregate types include list and map:
|
||||
|
||||
* list literals consist of zero or more values between brackets: "['a', 'b', 'c']"
|
||||
* map literal consist of colon-separated key-value pairs within braces: "{'key1': 1, 'key2': 2}"
|
||||
* Only int, uint, string, and bool types are valid map keys.
|
||||
* Maps containing HTTP headers must always use lower-cased string keys.
|
||||
|
||||
Comments start with two-forward slashes followed by text and a newline.`
|
||||
)
|
||||
|
||||
var (
|
||||
hiddenFunctions = map[string]bool{
|
||||
overloads.DeprecatedIn: true,
|
||||
operators.OldIn: true,
|
||||
operators.OldNotStrictlyFalse: true,
|
||||
operators.NotStrictlyFalse: true,
|
||||
}
|
||||
)
|
||||
// Copyright 2025 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
|
||||
//
|
||||
// https://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 cel
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
"github.com/google/cel-go/common/overloads"
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
//go:embed templates/authoring.tmpl
|
||||
var authoringPrompt string
|
||||
|
||||
// splitImpl splits a string into a list of strings.
|
||||
//
|
||||
// Normalizes extracted comments (trim common prefix whitespace and extra trailing newlines).
|
||||
func splitImpl(str string) []string {
|
||||
str = strings.TrimRight(str, " \n\t\r")
|
||||
out := strings.Split(str, "\n")
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
negative := strings.TrimLeft(out[0], " \t")
|
||||
lenNegative := len(negative)
|
||||
lenOut := len(out[0])
|
||||
if lenNegative == lenOut {
|
||||
return out
|
||||
}
|
||||
prefix := out[0][:lenOut-lenNegative]
|
||||
trimmed := make([]string, len(out))
|
||||
for i, line := range out {
|
||||
if line == "" {
|
||||
trimmed[i] = ""
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
return out
|
||||
}
|
||||
trimmed[i] = strings.TrimPrefix(line, prefix)
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// AuthoringPrompt creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
|
||||
func AuthoringPrompt(env *Env) (*Prompt, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"split": splitImpl,
|
||||
"newlineToSpace": func(str string) string { return strings.ReplaceAll(str, "\n", " ") },
|
||||
}
|
||||
tmpl := template.New("cel").Funcs(funcMap)
|
||||
tmpl, err := tmpl.Parse(authoringPrompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Prompt{
|
||||
Persona: defaultPersona,
|
||||
FormatRules: defaultFormatRules,
|
||||
GeneralUsage: defaultGeneralUsage,
|
||||
tmpl: tmpl,
|
||||
env: env,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthoringPromptWithFieldPaths creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
|
||||
// Includes documentation for all of the reachable field paths in the environment.
|
||||
func AuthoringPromptWithFieldPaths(env *Env) (*Prompt, error) {
|
||||
p, err := AuthoringPrompt(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.fieldPaths = true
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Prompt represents the core components of an LLM prompt based on a CEL environment.
|
||||
//
|
||||
// All fields of the prompt may be overwritten / modified with support for rendering the
|
||||
// prompt to a human-readable string.
|
||||
type Prompt struct {
|
||||
// Persona indicates something about the kind of user making the request
|
||||
Persona string
|
||||
|
||||
// FormatRules indicate how the LLM should generate its output
|
||||
FormatRules string
|
||||
|
||||
// GeneralUsage specifies additional context on how CEL should be used.
|
||||
GeneralUsage string
|
||||
|
||||
// tmpl is the text template base-configuration for rendering text.
|
||||
tmpl *template.Template
|
||||
|
||||
// fieldPaths is a flag to include reachable field paths in the prompt.
|
||||
fieldPaths bool
|
||||
|
||||
// env reference used to collect variables, functions, and macros available to the prompt.
|
||||
env *Env
|
||||
}
|
||||
|
||||
type promptVariable struct {
|
||||
*common.Doc
|
||||
FieldPaths []*common.Doc
|
||||
}
|
||||
|
||||
type promptInst struct {
|
||||
*Prompt
|
||||
|
||||
Variables []*promptVariable
|
||||
Macros []*common.Doc
|
||||
Functions []*common.Doc
|
||||
UserPrompt string
|
||||
}
|
||||
|
||||
// Render renders the user prompt with the associated context from the prompt template
|
||||
// for use with LLM generators.
|
||||
//
|
||||
// User-supplied input is passed as template data via the UserPrompt field, which
|
||||
// Go's text/template renders as a literal string value. Template action delimiters
|
||||
// such as {{.Persona}} in the user prompt are never evaluated as template directives
|
||||
// because text/template only executes directives present in the template definition
|
||||
// itself, not in data values interpolated at render time.
|
||||
func (p *Prompt) Render(userPrompt string) string {
|
||||
var buffer strings.Builder
|
||||
vars := make([]*promptVariable, len(p.env.Variables()))
|
||||
for i, v := range p.env.Variables() {
|
||||
vars[i] = &promptVariable{Doc: v.Documentation()}
|
||||
if p.fieldPaths && v.Type().Kind() == types.StructKind {
|
||||
var fieldPaths []*common.Doc
|
||||
|
||||
paths := fieldPathsForType(p.env.CELTypeProvider(), v.Name(), v.Type())
|
||||
if len(paths) < 2 {
|
||||
paths = nil
|
||||
} else {
|
||||
// First path is the variable which is already documented.
|
||||
paths = paths[1:]
|
||||
}
|
||||
for _, path := range paths {
|
||||
fieldPaths = append(fieldPaths, path.Documentation())
|
||||
}
|
||||
|
||||
sort.SliceStable(fieldPaths, func(i, j int) bool {
|
||||
return fieldPaths[i].Name < fieldPaths[j].Name
|
||||
})
|
||||
vars[i].FieldPaths = fieldPaths
|
||||
}
|
||||
}
|
||||
sort.SliceStable(vars, func(i, j int) bool {
|
||||
return vars[i].Name < vars[j].Name
|
||||
})
|
||||
macs := make([]*common.Doc, len(p.env.Macros()))
|
||||
for i, m := range p.env.Macros() {
|
||||
macs[i] = m.(common.Documentor).Documentation()
|
||||
}
|
||||
funcs := make([]*common.Doc, 0, len(p.env.Functions()))
|
||||
for _, f := range p.env.Functions() {
|
||||
if _, hidden := hiddenFunctions[f.Name()]; hidden {
|
||||
continue
|
||||
}
|
||||
funcs = append(funcs, f.Documentation())
|
||||
}
|
||||
sort.SliceStable(funcs, func(i, j int) bool {
|
||||
return funcs[i].Name < funcs[j].Name
|
||||
})
|
||||
inst := &promptInst{
|
||||
Prompt: p,
|
||||
Variables: vars,
|
||||
Macros: macs,
|
||||
Functions: funcs,
|
||||
UserPrompt: userPrompt,
|
||||
}
|
||||
p.tmpl.Execute(&buffer, inst)
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPersona = `You are a software engineer with expertise in networking and application security
|
||||
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
|
||||
networking, authentication, and data access is only permitted when all conditions
|
||||
are satisfied.`
|
||||
|
||||
defaultFormatRules = `Output your response as a CEL expression.
|
||||
|
||||
Write the expression with the comment on the first line and the expression on the
|
||||
subsequent lines. Format the expression using 80-character line limits commonly
|
||||
found in C++ or Java code.`
|
||||
|
||||
defaultGeneralUsage = `CEL supports Protocol Buffer and JSON types, as well as simple types and aggregate types.
|
||||
|
||||
Simple types include bool, bytes, double, int, string, and uint:
|
||||
|
||||
* double literals must always include a decimal point: 1.0, 3.5, -2.2
|
||||
* uint literals must be positive values suffixed with a 'u': 42u
|
||||
* byte literals are strings prefixed with a 'b': b'1235'
|
||||
* string literals can use either single quotes or double quotes: 'hello', "world"
|
||||
* string literals can also be treated as raw strings that do not require any
|
||||
escaping within the string by using the 'R' prefix: R"""quote: "hi" """
|
||||
|
||||
Aggregate types include list and map:
|
||||
|
||||
* list literals consist of zero or more values between brackets: "['a', 'b', 'c']"
|
||||
* map literal consist of colon-separated key-value pairs within braces: "{'key1': 1, 'key2': 2}"
|
||||
* Only int, uint, string, and bool types are valid map keys.
|
||||
* Maps containing HTTP headers must always use lower-cased string keys.
|
||||
|
||||
Comments start with two-forward slashes followed by text and a newline.`
|
||||
)
|
||||
|
||||
var (
|
||||
hiddenFunctions = map[string]bool{
|
||||
overloads.DeprecatedIn: true,
|
||||
operators.OldIn: true,
|
||||
operators.OldNotStrictlyFalse: true,
|
||||
operators.NotStrictlyFalse: true,
|
||||
}
|
||||
)
|
||||
+12
-4
@@ -791,18 +791,26 @@ func (c *coster) functionCost(e ast.Expr, function, overloadID string, target *A
|
||||
return CallEstimate{CostEstimate: c.sizeOrUnknown(args[1]).MultiplyByCostFactor(1).Add(argCostSum())}
|
||||
}
|
||||
// O(nm) functions
|
||||
case overloads.MatchesString:
|
||||
case overloads.Matches, overloads.MatchesString:
|
||||
// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
|
||||
if target != nil && len(args) == 1 {
|
||||
var strNode, regexNode AstNode
|
||||
if overloadID == overloads.MatchesString && target != nil && len(args) == 1 {
|
||||
strNode = *target
|
||||
regexNode = args[0]
|
||||
} else if overloadID == overloads.Matches && target == nil && len(args) == 2 {
|
||||
strNode = args[0]
|
||||
regexNode = args[1]
|
||||
}
|
||||
if strNode != nil && regexNode != nil {
|
||||
// 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 := c.sizeOrUnknown(*target).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor)
|
||||
strCost := c.sizeOrUnknown(strNode).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(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 := c.sizeOrUnknown(args[0]).MultiplyByCostFactor(common.RegexStringLengthCostFactor)
|
||||
regexCost := c.sizeOrUnknown(regexNode).MultiplyByCostFactor(common.RegexStringLengthCostFactor)
|
||||
return CallEstimate{CostEstimate: strCost.Multiply(regexCost).Add(argCostSum())}
|
||||
}
|
||||
case overloads.ContainsString:
|
||||
|
||||
+8
-7
@@ -227,7 +227,7 @@ func Abbrevs(qualifiedNames ...string) ContainerOption {
|
||||
}
|
||||
alias := qn[ind+1:]
|
||||
var err error
|
||||
c, err = aliasAs("abbreviation", qn, alias)(c)
|
||||
c, err = aliasAs("abbreviation", qn, alias, true)(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,31 +236,32 @@ func Abbrevs(qualifiedNames ...string) ContainerOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Alias associates a fully-qualified name with a user-defined alias.
|
||||
// Alias associates a name with a user-defined alias.
|
||||
//
|
||||
// In general, Abbrevs is preferred to Alias since the names generated from the Abbrevs option
|
||||
// are more easily traced back to source code. The Alias option is useful for propagating alias
|
||||
// configuration from one Container instance to another, and may also be useful for remapping
|
||||
// poorly chosen protobuf message / package names.
|
||||
//
|
||||
// Note: all of the rules that apply to Abbrevs also apply to Alias.
|
||||
func Alias(qualifiedName, alias string) ContainerOption {
|
||||
return aliasAs("alias", qualifiedName, alias)
|
||||
return aliasAs("alias", qualifiedName, alias, false)
|
||||
}
|
||||
|
||||
func aliasAs(kind, qualifiedName, alias string) ContainerOption {
|
||||
func aliasAs(kind, qualifiedName, alias string, requireQualified bool) ContainerOption {
|
||||
return func(c *Container) (*Container, error) {
|
||||
if len(alias) == 0 || strings.Contains(alias, ".") {
|
||||
return nil, fmt.Errorf(
|
||||
"%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias)
|
||||
}
|
||||
|
||||
if len(qualifiedName) == 0 {
|
||||
return nil, fmt.Errorf("%s must refer to a valid name: %s", kind, qualifiedName)
|
||||
}
|
||||
if qualifiedName[0:1] == "." {
|
||||
return nil, fmt.Errorf("qualified name must not begin with a leading '.': %s",
|
||||
qualifiedName)
|
||||
}
|
||||
ind := strings.LastIndex(qualifiedName, ".")
|
||||
if ind <= 0 || ind == len(qualifiedName)-1 {
|
||||
if ind == len(qualifiedName)-1 || (requireQualified && ind <= 0) {
|
||||
return nil, fmt.Errorf("%s must refer to a valid qualified name: %s",
|
||||
kind, qualifiedName)
|
||||
}
|
||||
|
||||
+26
-6
@@ -15,7 +15,11 @@
|
||||
// Package functions defines the standard builtin functions supported by the interpreter
|
||||
package functions
|
||||
|
||||
import "github.com/google/cel-go/common/types/ref"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -41,21 +45,37 @@ type Overload struct {
|
||||
// Binary defines the overload with a BinaryOp implementation. May be nil.
|
||||
Binary BinaryOp
|
||||
|
||||
// Function defines the overload with a FunctionOp implementation. May be
|
||||
// nil.
|
||||
// Function defines the overload with a FunctionOp implementation. May be nil.
|
||||
Function FunctionOp
|
||||
|
||||
// Async defines the overload with an AsyncOp implementation. May be nil.
|
||||
Async AsyncOp
|
||||
|
||||
// NonStrict specifies whether the Overload will tolerate arguments that
|
||||
// are types.Err or types.Unknown.
|
||||
NonStrict bool
|
||||
}
|
||||
|
||||
// UnaryOp is a function that takes a single value and produces an output.
|
||||
type UnaryOp func(value ref.Val) ref.Val
|
||||
type UnaryOp func(ref.Val) ref.Val
|
||||
|
||||
// BinaryOp is a function that takes two values and produces an output.
|
||||
type BinaryOp func(lhs ref.Val, rhs ref.Val) ref.Val
|
||||
type BinaryOp func(ref.Val, ref.Val) ref.Val
|
||||
|
||||
// FunctionOp is a function with accepts zero or more arguments and produces
|
||||
// a value or error as a result.
|
||||
type FunctionOp func(values ...ref.Val) ref.Val
|
||||
type FunctionOp func(...ref.Val) ref.Val
|
||||
|
||||
// AsyncOp is a function that accepts zero or more arguments and produces
|
||||
// a value or error asynchronously via a channel.
|
||||
//
|
||||
// AsyncOp is an internal interface intended for use by CEL to manage goroutines and
|
||||
// channels associated with async calls. For public API usage, use BlockingAsyncOp.
|
||||
// Implementers should listen for context cancellation on the provided context for
|
||||
// resource cleanup.
|
||||
type AsyncOp func(context.Context, ...ref.Val) <-chan ref.Val
|
||||
|
||||
// BlockingAsyncOp is a function that accepts zero or more arguments and blocks until
|
||||
// the result is available. When used with AsyncBinding, the framework runs the function
|
||||
// in its own goroutine and manages channel lifecycle internally.
|
||||
type BlockingAsyncOp func(context.Context, ...ref.Val) ref.Val
|
||||
|
||||
+52
-30
@@ -16,6 +16,7 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -113,45 +114,64 @@ var _ Buffer = &supplementalBuffer{}
|
||||
|
||||
var nilBuffer = &emptyBuffer{}
|
||||
|
||||
// SizeLimitError indicates that the input exceeded the configured code point limit.
|
||||
type SizeLimitError struct {
|
||||
Size int
|
||||
Limit int
|
||||
}
|
||||
|
||||
func (e *SizeLimitError) Error() string {
|
||||
return fmt.Sprintf("expression code point size exceeds limit: size: %d, limit %d", e.Size, e.Limit)
|
||||
}
|
||||
|
||||
// NewBuffer returns an efficient implementation of Buffer for the given text based on the ranges of
|
||||
// the encoded code points contained within.
|
||||
//
|
||||
// Code points are represented as an array of byte, uint16, or rune. This approach ensures that
|
||||
// each index represents a code point by itself without needing to use an array of rune. At first
|
||||
// we assume all code points are less than or equal to '\u007f'. If this holds true, the
|
||||
// underlying storage is a byte array containing only ASCII characters. If we encountered a code
|
||||
// point above this range but less than or equal to '\uffff' we allocate a uint16 array, copy the
|
||||
// elements of previous byte array to the uint16 array, and continue. If this holds true, the
|
||||
// underlying storage is a uint16 array containing only Unicode characters in the Basic Multilingual
|
||||
// Plane. If we encounter a code point above '\uffff' we allocate an rune array, copy the previous
|
||||
// elements of the byte or uint16 array, and continue. The underlying storage is an rune array
|
||||
// containing any Unicode character.
|
||||
func NewBuffer(data string) Buffer {
|
||||
buf, _ := newBuffer(data, false)
|
||||
buf, _, _ := newBufferWithLimit(data, false, -1)
|
||||
return buf
|
||||
}
|
||||
|
||||
// NewBufferAndLineOffsets returns an efficient implementation of Buffer for the given text based on
|
||||
// the ranges of the encoded code points contained within, as well as returning the line offsets.
|
||||
//
|
||||
// Code points are represented as an array of byte, uint16, or rune. This approach ensures that
|
||||
// each index represents a code point by itself without needing to use an array of rune. At first
|
||||
// we assume all code points are less than or equal to '\u007f'. If this holds true, the
|
||||
// underlying storage is a byte array containing only ASCII characters. If we encountered a code
|
||||
// point above this range but less than or equal to '\uffff' we allocate a uint16 array, copy the
|
||||
// elements of previous byte array to the uint16 array, and continue. If this holds true, the
|
||||
// underlying storage is a uint16 array containing only Unicode characters in the Basic Multilingual
|
||||
// Plane. If we encounter a code point above '\uffff' we allocate an rune array, copy the previous
|
||||
// elements of the byte or uint16 array, and continue. The underlying storage is an rune array
|
||||
// containing any Unicode character.
|
||||
func NewBufferAndLineOffsets(data string) (Buffer, []int32) {
|
||||
return newBuffer(data, true)
|
||||
buf, offs, _ := newBufferWithLimit(data, true, -1)
|
||||
return buf, offs
|
||||
}
|
||||
|
||||
func newBuffer(data string, lines bool) (Buffer, []int32) {
|
||||
if len(data) == 0 {
|
||||
return nilBuffer, []int32{0}
|
||||
// NewBufferAndLineOffsetsWithLimit returns an efficient implementation of Buffer for the given text
|
||||
// and enforces a code point limit while constructing the buffer.
|
||||
func NewBufferAndLineOffsetsWithLimit(data string, limit int) (Buffer, []int32, error) {
|
||||
if limit < 0 || len(data) <= limit {
|
||||
return newBufferWithLimit(data, true, -1)
|
||||
}
|
||||
return newBufferWithLimit(data, true, limit)
|
||||
}
|
||||
|
||||
func countRemainingCodePoints(data string, idx int, count int) int {
|
||||
for idx < len(data) {
|
||||
_, s := utf8.DecodeRuneInString(data[idx:])
|
||||
idx += s
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func newBufferWithLimit(data string, lines bool, limit int) (Buffer, []int32, error) {
|
||||
if len(data) == 0 {
|
||||
return nilBuffer, []int32{0}, nil
|
||||
}
|
||||
if limit >= 0 && len(data) > limit {
|
||||
size := countRemainingCodePoints(data, 0, 0)
|
||||
if size > limit {
|
||||
return nil, nil, &SizeLimitError{
|
||||
Size: size,
|
||||
Limit: limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The resulting buffers store one element per code point, so the worst case
|
||||
// element count never exceeds len(data).
|
||||
var (
|
||||
idx = 0
|
||||
off int32 = 0
|
||||
@@ -195,7 +215,8 @@ func newBuffer(data string, lines bool) (Buffer, []int32) {
|
||||
}
|
||||
return &asciiBuffer{
|
||||
arr: buf8,
|
||||
}, offs
|
||||
}, offs, nil
|
||||
|
||||
copy16:
|
||||
for idx < len(data) {
|
||||
r, s := utf8.DecodeRuneInString(data[idx:])
|
||||
@@ -222,7 +243,8 @@ copy16:
|
||||
}
|
||||
return &basicBuffer{
|
||||
arr: buf16,
|
||||
}, offs
|
||||
}, offs, nil
|
||||
|
||||
copy32:
|
||||
for idx < len(data) {
|
||||
r, s := utf8.DecodeRuneInString(data[idx:])
|
||||
@@ -238,5 +260,5 @@ copy32:
|
||||
}
|
||||
return &supplementalBuffer{
|
||||
arr: buf32,
|
||||
}, offs
|
||||
}, offs, nil
|
||||
}
|
||||
|
||||
+23
@@ -74,6 +74,12 @@ func NewTextSource(text string) Source {
|
||||
return NewStringSource(text, "<input>")
|
||||
}
|
||||
|
||||
// NewTextSourceWithLimit creates a new Source from the input text string while
|
||||
// enforcing a maximum code point count when needed.
|
||||
func NewTextSourceWithLimit(text string, limit int) (Source, error) {
|
||||
return NewStringSourceWithLimit(text, "<input>", limit)
|
||||
}
|
||||
|
||||
// NewStringSource creates a new Source from the given contents and description.
|
||||
func NewStringSource(contents string, description string) Source {
|
||||
// Compute line offsets up front as they are referred to frequently.
|
||||
@@ -85,6 +91,23 @@ func NewStringSource(contents string, description string) Source {
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringSourceWithLimit creates a new Source from the given contents and
|
||||
// description while enforcing a maximum code point count when needed.
|
||||
func NewStringSourceWithLimit(contents string, description string, limit int) (Source, error) {
|
||||
if limit < 0 || len(contents) <= limit {
|
||||
return NewStringSource(contents, description), nil
|
||||
}
|
||||
buf, offs, err := runes.NewBufferAndLineOffsetsWithLimit(contents, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sourceImpl{
|
||||
Buffer: buf,
|
||||
description: description,
|
||||
lineOffsets: offs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewInfoSource creates a new Source from a SourceInfo.
|
||||
func NewInfoSource(info *exprpb.SourceInfo) Source {
|
||||
return &sourceImpl{
|
||||
|
||||
+3
@@ -302,6 +302,9 @@ func timeZone(tz ref.Val, visitor timestampVisitor) timestampVisitor {
|
||||
if err != nil {
|
||||
return WrapErr(err)
|
||||
}
|
||||
if min < 0 || min > 59 {
|
||||
return WrapErr(fmt.Errorf("timezone offset minutes out of range [0, 59]: %s", val))
|
||||
}
|
||||
var offset int
|
||||
if string(val[0]) == "-" {
|
||||
offset = hr*60 - min
|
||||
|
||||
+14
@@ -181,6 +181,20 @@ func (u *Unknown) GetAttributeTrails(id int64) ([]*AttributeTrail, bool) {
|
||||
return trails, found
|
||||
}
|
||||
|
||||
// HasUnknownFunction returns whether any of the attribute trails contained within the unknown
|
||||
// are unspecified. Unspecified attributes typically indicate an unresolved function call
|
||||
// or operation, rather than a missing variable.
|
||||
func (u *Unknown) HasUnknownFunction() bool {
|
||||
for _, trails := range u.attributeTrails {
|
||||
for _, t := range trails {
|
||||
if t.variable == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Contains returns true if the input unknown is a subset of the current unknown.
|
||||
func (u *Unknown) Contains(other *Unknown) bool {
|
||||
for id, otherTrails := range other.attributeTrails {
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ go_library(
|
||||
"lists.go",
|
||||
"math.go",
|
||||
"native.go",
|
||||
"network.go",
|
||||
"protos.go",
|
||||
"regex.go",
|
||||
"sets.go",
|
||||
@@ -40,6 +41,7 @@ go_library(
|
||||
"//common/types/traits:go_default_library",
|
||||
"//interpreter:go_default_library",
|
||||
"//parser:go_default_library",
|
||||
"@org_golang_google_protobuf//encoding/protojson:go_default_library",
|
||||
"@org_golang_google_protobuf//proto:go_default_library",
|
||||
"@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
|
||||
"@org_golang_google_protobuf//types/known/structpb",
|
||||
@@ -61,6 +63,7 @@ go_test(
|
||||
"lists_test.go",
|
||||
"math_test.go",
|
||||
"native_test.go",
|
||||
"network_test.go",
|
||||
"protos_test.go",
|
||||
"regex_test.go",
|
||||
"sets_test.go",
|
||||
|
||||
+14
@@ -55,6 +55,20 @@ Example:
|
||||
|
||||
base64.encode(b'hello') // return 'aGVsbG8='
|
||||
|
||||
### JSON.Encode
|
||||
|
||||
Introduced at version: 1
|
||||
|
||||
Encodes a CEL value to a JSON string.
|
||||
|
||||
json.encode(<dyn>) -> <string>
|
||||
|
||||
Examples:
|
||||
|
||||
json.encode('hello') // return '"hello"'
|
||||
json.encode([1, 'two', true]) // return '[1,"two",true]'
|
||||
json.encode({'items': [1, 'two', false]}) // return '{"items":[1,"two",false]}'
|
||||
|
||||
## Math
|
||||
|
||||
Math helper macros and functions.
|
||||
|
||||
+37
-15
@@ -108,7 +108,7 @@ func (lib *celBindings) CompileOptions() []cel.EnvOption {
|
||||
|
||||
func (lib *celBindings) ProgramOptions() []cel.ProgramOption {
|
||||
if lib.version >= 1 {
|
||||
celBlockPlan := func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
|
||||
celBlockPlan := func(i interpreter.InterpretableV2) (interpreter.InterpretableV2, error) {
|
||||
call, ok := i.(interpreter.InterpretableCall)
|
||||
if !ok {
|
||||
return i, nil
|
||||
@@ -140,7 +140,7 @@ func (lib *celBindings) ProgramOptions() []cel.ProgramOption {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return []cel.ProgramOption{cel.CustomDecorator(celBlockPlan)}
|
||||
return []cel.ProgramOption{cel.CustomDecoratorV2(celBlockPlan)}
|
||||
}
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func celBind(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Ex
|
||||
), nil
|
||||
}
|
||||
|
||||
func newDynamicBlock(slotExprs []interpreter.Interpretable, expr interpreter.Interpretable) interpreter.Interpretable {
|
||||
func newDynamicBlock(slotExprs []interpreter.InterpretableV2, expr interpreter.InterpretableV2) interpreter.InterpretableV2 {
|
||||
bs := &dynamicBlock{
|
||||
slotExprs: slotExprs,
|
||||
expr: expr,
|
||||
@@ -213,8 +213,8 @@ func newDynamicBlock(slotExprs []interpreter.Interpretable, expr interpreter.Int
|
||||
}
|
||||
|
||||
type dynamicBlock struct {
|
||||
slotExprs []interpreter.Interpretable
|
||||
expr interpreter.Interpretable
|
||||
slotExprs []interpreter.InterpretableV2
|
||||
expr interpreter.InterpretableV2
|
||||
slotActivationPool *sync.Pool
|
||||
}
|
||||
|
||||
@@ -223,12 +223,23 @@ func (b *dynamicBlock) ID() int64 {
|
||||
return b.expr.ID()
|
||||
}
|
||||
|
||||
// Exec implements the Interpretable interface method and pushes a new frame onto the
|
||||
// execution frame for the duration of the block execution.
|
||||
func (b *dynamicBlock) Exec(frame *interpreter.ExecutionFrame) ref.Val {
|
||||
sa := b.slotActivationPool.Get().(*dynamicSlotActivation)
|
||||
sa.frame = frame.Push(sa)
|
||||
// Ensure the 'unwrapped' Activation points to the original one from the frame,
|
||||
// and not the hierarchical activation which composes the original and the slot
|
||||
// activation.
|
||||
sa.Activation = frame.Activation
|
||||
defer sa.frame.Pop()
|
||||
defer b.clearSlots(sa)
|
||||
return b.expr.Exec(sa.frame)
|
||||
}
|
||||
|
||||
// Eval implements the Interpretable interface method.
|
||||
func (b *dynamicBlock) Eval(activation cel.Activation) ref.Val {
|
||||
sa := b.slotActivationPool.Get().(*dynamicSlotActivation)
|
||||
sa.Activation = activation
|
||||
defer b.clearSlots(sa)
|
||||
return b.expr.Eval(sa)
|
||||
return b.Exec(interpreter.AsFrame(activation))
|
||||
}
|
||||
|
||||
func (b *dynamicBlock) clearSlots(sa *dynamicSlotActivation) {
|
||||
@@ -243,7 +254,8 @@ type slotVal struct {
|
||||
|
||||
type dynamicSlotActivation struct {
|
||||
cel.Activation
|
||||
slotExprs []interpreter.Interpretable
|
||||
frame *interpreter.ExecutionFrame
|
||||
slotExprs []interpreter.InterpretableV2
|
||||
slotCount int
|
||||
slotVals []*slotVal
|
||||
}
|
||||
@@ -267,7 +279,7 @@ func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) {
|
||||
return *v.value, true
|
||||
}
|
||||
v.visited = true
|
||||
val := sa.slotExprs[idx].Eval(sa)
|
||||
val := sa.slotExprs[idx].Exec(sa.frame)
|
||||
v.value = &val
|
||||
return val, true
|
||||
}
|
||||
@@ -276,13 +288,14 @@ func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) {
|
||||
|
||||
func (sa *dynamicSlotActivation) reset() {
|
||||
sa.Activation = nil
|
||||
sa.frame = nil
|
||||
for _, sv := range sa.slotVals {
|
||||
sv.visited = false
|
||||
sv.value = nil
|
||||
}
|
||||
}
|
||||
|
||||
func newConstantBlock(slots traits.Lister, expr interpreter.Interpretable) interpreter.Interpretable {
|
||||
func newConstantBlock(slots traits.Lister, expr interpreter.InterpretableV2) interpreter.InterpretableV2 {
|
||||
count := slots.Size().(types.Int)
|
||||
return &constantBlock{slots: slots, slotCount: int(count), expr: expr}
|
||||
}
|
||||
@@ -290,7 +303,7 @@ func newConstantBlock(slots traits.Lister, expr interpreter.Interpretable) inter
|
||||
type constantBlock struct {
|
||||
slots traits.Lister
|
||||
slotCount int
|
||||
expr interpreter.Interpretable
|
||||
expr interpreter.InterpretableV2
|
||||
}
|
||||
|
||||
// ID implements the interpreter.Interpretable interface method.
|
||||
@@ -298,15 +311,24 @@ func (b *constantBlock) ID() int64 {
|
||||
return b.expr.ID()
|
||||
}
|
||||
|
||||
// Exec implements the Interpretable interface method and pushes a new frame onto the
|
||||
// stack for the duration of the block execution.
|
||||
func (b *constantBlock) Exec(frame *interpreter.ExecutionFrame) ref.Val {
|
||||
sa := constantSlotActivation{Activation: frame.Activation, slots: b.slots, slotCount: b.slotCount}
|
||||
sa.frame = frame.Push(sa)
|
||||
defer sa.frame.Pop()
|
||||
return b.expr.Exec(sa.frame)
|
||||
}
|
||||
|
||||
// Eval implements the interpreter.Interpretable interface method, and will proxy @index prefixed variable
|
||||
// lookups into a set of constant slots determined from the plan step.
|
||||
func (b *constantBlock) Eval(activation cel.Activation) ref.Val {
|
||||
vars := constantSlotActivation{Activation: activation, slots: b.slots, slotCount: b.slotCount}
|
||||
return b.expr.Eval(vars)
|
||||
return b.Exec(interpreter.AsFrame(activation))
|
||||
}
|
||||
|
||||
type constantSlotActivation struct {
|
||||
cel.Activation
|
||||
frame *interpreter.ExecutionFrame
|
||||
slots traits.Lister
|
||||
slotCount int
|
||||
}
|
||||
|
||||
+43
-2
@@ -16,11 +16,14 @@ package ext
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// Encoders returns a cel.EnvOption to configure extended functions for string, byte, and object
|
||||
@@ -48,6 +51,18 @@ import (
|
||||
// Examples:
|
||||
//
|
||||
// base64.encode(b'hello') // return b'aGVsbG8='
|
||||
//
|
||||
// # JSON.Encode
|
||||
//
|
||||
// Introduced at version: 1
|
||||
//
|
||||
// Encodes a CEL value to a JSON string.
|
||||
//
|
||||
// json.encode(<dyn>) -> <string>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// json.encode({'hello': 'world'}) // return '{"hello":"world"}'
|
||||
func Encoders(options ...EncodersOption) cel.EnvOption {
|
||||
l := &encoderLib{version: math.MaxUint32}
|
||||
for _, o := range options {
|
||||
@@ -75,8 +90,8 @@ func (*encoderLib) LibraryName() string {
|
||||
return "cel.lib.ext.encoders"
|
||||
}
|
||||
|
||||
func (*encoderLib) CompileOptions() []cel.EnvOption {
|
||||
return []cel.EnvOption{
|
||||
func (lib *encoderLib) CompileOptions() []cel.EnvOption {
|
||||
opts := []cel.EnvOption{
|
||||
cel.Function("base64.decode",
|
||||
cel.Overload("base64_decode_string", []*cel.Type{cel.StringType}, cel.BytesType,
|
||||
cel.UnaryBinding(func(str ref.Val) ref.Val {
|
||||
@@ -90,6 +105,16 @@ func (*encoderLib) CompileOptions() []cel.EnvOption {
|
||||
return stringOrError(base64EncodeBytes([]byte(b)))
|
||||
}))),
|
||||
}
|
||||
if lib.version >= 1 {
|
||||
opts = append(opts,
|
||||
cel.Function("json.encode",
|
||||
cel.Overload("json_encode_dyn", []*cel.Type{cel.DynType}, cel.StringType,
|
||||
cel.UnaryBinding(func(val ref.Val) ref.Val {
|
||||
return stringOrError(jsonEncodeValue(val))
|
||||
}))),
|
||||
)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func (*encoderLib) ProgramOptions() []cel.ProgramOption {
|
||||
@@ -110,3 +135,19 @@ func base64DecodeString(str string) ([]byte, error) {
|
||||
func base64EncodeBytes(bytes []byte) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func jsonEncodeValue(val ref.Val) (string, error) {
|
||||
native, err := val.ConvertToNative(types.JSONValueType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsonValue, ok := native.(*structpb.Value)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cannot convert %T to JSON value", native)
|
||||
}
|
||||
jsonBytes, err := protojson.Marshal(jsonValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
+25
-5
@@ -153,15 +153,18 @@ var comparableTypes = []*cel.Type{
|
||||
// ].sortBy(e, e.score).map(e, e.name)
|
||||
// == ["bar", "foo", "baz"]
|
||||
func Lists(options ...ListsOption) cel.EnvOption {
|
||||
l := &listsLib{version: math.MaxUint32}
|
||||
l := &listsLib{version: math.MaxUint32, maxRangeSize: defaultMaxRangeSize}
|
||||
for _, o := range options {
|
||||
l = o(l)
|
||||
}
|
||||
return cel.Lib(l)
|
||||
}
|
||||
|
||||
const defaultMaxRangeSize = 1_000_000
|
||||
|
||||
type listsLib struct {
|
||||
version uint32
|
||||
version uint32
|
||||
maxRangeSize int64
|
||||
}
|
||||
|
||||
// LibraryName implements the SingletonLibrary interface method.
|
||||
@@ -188,6 +191,16 @@ func ListsVersion(version uint32) ListsOption {
|
||||
}
|
||||
}
|
||||
|
||||
// ListsMaxRangeSize sets the maximum number of elements lists.range() will
|
||||
// allocate. If not set, the default is 10,000,000. Setting this to zero
|
||||
// disables the limit (not recommended).
|
||||
func ListsMaxRangeSize(size int64) ListsOption {
|
||||
return func(lib *listsLib) *listsLib {
|
||||
lib.maxRangeSize = size
|
||||
return lib
|
||||
}
|
||||
}
|
||||
|
||||
// CompileOptions implements the Library interface method.
|
||||
func (lib listsLib) CompileOptions() []cel.EnvOption {
|
||||
listType := cel.ListType(cel.TypeParamType("T"))
|
||||
@@ -309,11 +322,12 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
|
||||
)...,
|
||||
))
|
||||
|
||||
maxRange := lib.maxRangeSize
|
||||
opts = append(opts, cel.Function("lists.range",
|
||||
cel.Overload("lists_range",
|
||||
[]*cel.Type{cel.IntType}, cel.ListType(cel.IntType),
|
||||
cel.UnaryBinding(func(n ref.Val) ref.Val {
|
||||
result, err := genRange(n.(types.Int))
|
||||
result, err := genRange(n.(types.Int), maxRange)
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
@@ -403,8 +417,14 @@ func (lib *listsLib) ProgramOptions() []cel.ProgramOption {
|
||||
return opts
|
||||
}
|
||||
|
||||
func genRange(n types.Int) (ref.Val, error) {
|
||||
var newList []ref.Val
|
||||
func genRange(n types.Int, maxSize int64) (ref.Val, error) {
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("lists.range: size must be non-negative, got %d", n)
|
||||
}
|
||||
if maxSize > 0 && int64(n) > maxSize {
|
||||
return nil, fmt.Errorf("lists.range: size %d exceeds maximum allowed (%d)", n, maxSize)
|
||||
}
|
||||
newList := make([]ref.Val, 0, n)
|
||||
for i := types.Int(0); i < n; i++ {
|
||||
newList = append(newList, i)
|
||||
}
|
||||
|
||||
+619
@@ -0,0 +1,619 @@
|
||||
// Copyright 2025 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 ext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/ast"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version1 is the initial version of the Network library, providing
|
||||
// parity with Kubernetes v1.30+ CEL network functions.
|
||||
Version1 uint32 = 1
|
||||
)
|
||||
|
||||
// Network returns a cel.EnvOption to configure extended functions for network
|
||||
// address parsing, inspection, and CIDR range manipulation.
|
||||
//
|
||||
// Note: This library defines global functions `ip`, `cidr`, `isIP`, `isCIDR`
|
||||
// and `ip.isCanonical`. If you are currently using variables named `ip` or
|
||||
// `cidr`, these functions will likely work as intended, however there is a
|
||||
// chance for collision.
|
||||
//
|
||||
// The library closely mirrors the behavior of the Kubernetes CEL network
|
||||
// libraries, treating IP addresses and CIDR ranges as opaque types. It parses
|
||||
// IPs strictly: IPv4-mapped IPv6 addresses and IP zones are not allowed.
|
||||
//
|
||||
// This library includes a TypeAdapter that allows `netip.Addr` and
|
||||
// `netip.Prefix` Go types to be passed directly into the CEL environment.
|
||||
//
|
||||
// # IP Addresses
|
||||
//
|
||||
// The `ip` function converts a string to an IP address (IPv4 or IPv6). If the
|
||||
// string is not a valid IP, an error is returned. The `isIP` function checks
|
||||
// if a string is a valid IP address without throwing an error.
|
||||
//
|
||||
// ip(string) -> ip
|
||||
// isIP(string) -> bool
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// ip('127.0.0.1')
|
||||
// ip('::1')
|
||||
// isIP('1.2.3.4') // true
|
||||
// isIP('invalid') // false
|
||||
//
|
||||
// # CIDR Ranges
|
||||
//
|
||||
// The `cidr` function converts a string to a Classless Inter-Domain Routing
|
||||
// (CIDR) range. If the string is not valid, an error is returned.
|
||||
//
|
||||
// The `isCIDR` function checks if a string is a valid CIDR notation. Note that
|
||||
// `isCIDR` allows CIDR values with or without host bits (e.g., '10.0.0.1/8'
|
||||
// or '10.0.0.0/8').
|
||||
//
|
||||
// cidr(string) -> cidr
|
||||
// isCIDR(string) -> bool
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// cidr('192.168.0.0/24')
|
||||
// cidr('::1/128')
|
||||
// isCIDR('10.0.0.0/8') // true
|
||||
// isCIDR('10.0.0.1/8') // true
|
||||
//
|
||||
// # IP Inspection and Canonicalization
|
||||
//
|
||||
// IP objects support various inspection methods.
|
||||
//
|
||||
// <ip>.family() -> int
|
||||
// <ip>.isLoopback() -> bool
|
||||
// <ip>.isGlobalUnicast() -> bool
|
||||
// <ip>.isLinkLocalMulticast() -> bool
|
||||
// <ip>.isLinkLocalUnicast() -> bool
|
||||
// <ip>.isUnspecified() -> bool
|
||||
//
|
||||
// The `ip.isCanonical` function takes a string and returns true if it matches
|
||||
// the RFC 5952 canonical string representation of that address.
|
||||
//
|
||||
// ip.isCanonical(string) -> bool
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// ip('127.0.0.1').family() == 4
|
||||
// ip('::1').family() == 6
|
||||
// ip('127.0.0.1').isLoopback() == true
|
||||
// ip.isCanonical('2001:db8::1') == true // RFC 5952 format
|
||||
// ip.isCanonical('2001:DB8::1') == false // Uppercase is not canonical
|
||||
// ip.isCanonical('2001:db8:0:0:0:0:0:1') == false // Expanded is not canonical
|
||||
//
|
||||
// # CIDR Member Functions
|
||||
//
|
||||
// CIDR objects support containment checks and property extraction.
|
||||
//
|
||||
// <cidr>.containsIP(ip|string) -> bool
|
||||
// <cidr>.containsCIDR(cidr|string) -> bool
|
||||
// <cidr>.ip() -> ip
|
||||
// <cidr>.isMask() -> bool
|
||||
// <cidr>.masked() -> cidr
|
||||
// <cidr>.prefixLength() -> int
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// cidr('10.0.0.0/8').containsIP(ip('10.0.0.1')) == true
|
||||
// cidr('10.0.0.0/8').containsIP('10.0.0.1') == true
|
||||
// cidr('10.0.0.0/8').containsCIDR('10.1.0.0/16') == true
|
||||
// cidr('192.168.1.5/24').ip() == ip('192.168.1.5')
|
||||
// cidr('192.168.1.0/24').isMask() == true
|
||||
// cidr('192.168.1.5/24').isMask() == false
|
||||
// cidr('192.168.1.5/24').masked() == cidr('192.168.1.0/24')
|
||||
// cidr('192.168.1.0/24').prefixLength() == 24
|
||||
func Network(opts ...NetworkOption) cel.EnvOption {
|
||||
lib := &networkLib{version: Version1}
|
||||
for _, o := range opts {
|
||||
lib = o(lib)
|
||||
}
|
||||
return func(e *cel.Env) (*cel.Env, error) {
|
||||
// Install the library (Types and Functions)
|
||||
e, err := cel.Lib(lib)(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Install the Adapter (Wrapping the existing one)
|
||||
adapter := &networkAdapter{Adapter: e.CELTypeAdapter()}
|
||||
return cel.CustomTypeAdapter(adapter)(e)
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkOption declares a functional operator for configuring the Network library behavior.
|
||||
type NetworkOption func(*networkLib) *networkLib
|
||||
|
||||
// NetworkVersion sets the version of the network library to an explicit version.
|
||||
func NetworkVersion(version uint32) NetworkOption {
|
||||
return func(lib *networkLib) *networkLib {
|
||||
lib.version = version
|
||||
return lib
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// Function names matching the original Kubernetes implementation of this networking library.
|
||||
// isStrictCIDR and isInterfaceAddress are added to enable strict isCIDR parsing without breaking
|
||||
// functionality for existing users. Ctx: https://github.com/kubernetes/kubernetes/issues/134224
|
||||
cidrFunc = "cidr"
|
||||
cidrToString = "string"
|
||||
containsCIDRFunc = "containsCIDR"
|
||||
containsIPFunc = "containsIP"
|
||||
familyFunc = "family"
|
||||
ipFunc = "ip"
|
||||
ipToString = "string"
|
||||
isCanonicalFunc = "ip.isCanonical"
|
||||
isCIDRFunc = "isCIDR"
|
||||
isGlobalUnicastFunc = "isGlobalUnicast"
|
||||
isIPFunc = "isIP"
|
||||
isLinkLocalMcastFunc = "isLinkLocalMulticast"
|
||||
isLinkLocalUcastFunc = "isLinkLocalUnicast"
|
||||
isLoopbackFunc = "isLoopback"
|
||||
isMaskFunc = "isMask"
|
||||
isUnspecifiedFunc = "isUnspecified"
|
||||
maskedFunc = "masked"
|
||||
prefixLengthFunc = "prefixLength"
|
||||
)
|
||||
|
||||
var (
|
||||
// Definitions for the Opaque Types
|
||||
IPType = types.NewOpaqueType("net.IP")
|
||||
CIDRType = types.NewOpaqueType("net.CIDR")
|
||||
)
|
||||
|
||||
type networkLib struct {
|
||||
version uint32
|
||||
}
|
||||
|
||||
func (*networkLib) LibraryName() string {
|
||||
return "cel.lib.ext.network"
|
||||
}
|
||||
|
||||
func (*networkLib) CompileOptions() []cel.EnvOption {
|
||||
return []cel.EnvOption{
|
||||
// 1. Register Types
|
||||
cel.Types(
|
||||
IPType,
|
||||
CIDRType,
|
||||
),
|
||||
|
||||
// 2. Register Functions
|
||||
cel.Function(cidrFunc,
|
||||
// K8s Parity: Following the pattern, this is "string_to_cidr"
|
||||
cel.Overload("string_to_cidr", []*cel.Type{cel.StringType}, CIDRType,
|
||||
cel.UnaryBinding(netCIDRString)),
|
||||
),
|
||||
cel.Function(cidrToString,
|
||||
cel.Overload("cidr_to_string", []*cel.Type{CIDRType}, cel.StringType,
|
||||
cel.UnaryBinding(netCIDRToString)),
|
||||
),
|
||||
cel.Function(containsCIDRFunc,
|
||||
cel.MemberOverload("cidr_contains_cidr", []*cel.Type{CIDRType, CIDRType}, cel.BoolType,
|
||||
cel.BinaryBinding(netCIDRContainsCIDR)),
|
||||
cel.MemberOverload("cidr_contains_cidr_string", []*cel.Type{CIDRType, cel.StringType}, cel.BoolType,
|
||||
cel.BinaryBinding(netCIDRContainsCIDRString)),
|
||||
),
|
||||
cel.Function(containsIPFunc,
|
||||
cel.MemberOverload("cidr_contains_ip_ip", []*cel.Type{CIDRType, IPType}, cel.BoolType,
|
||||
cel.BinaryBinding(netCIDRContainsIP)),
|
||||
cel.MemberOverload("cidr_contains_ip_string", []*cel.Type{CIDRType, cel.StringType}, cel.BoolType,
|
||||
cel.BinaryBinding(netCIDRContainsIPString)),
|
||||
),
|
||||
cel.Function(familyFunc,
|
||||
cel.MemberOverload("ip_family", []*cel.Type{IPType}, cel.IntType,
|
||||
cel.UnaryBinding(netIPFamily)),
|
||||
),
|
||||
cel.Function(ipFunc,
|
||||
// K8s Parity: The global overload is named "string_to_ip"
|
||||
cel.Overload("string_to_ip", []*cel.Type{cel.StringType}, IPType,
|
||||
cel.UnaryBinding(netIPString)),
|
||||
// K8s Parity: The member overload is named "cidr_ip"
|
||||
cel.MemberOverload("cidr_ip", []*cel.Type{CIDRType}, IPType,
|
||||
cel.UnaryBinding(netCIDRIP)),
|
||||
),
|
||||
cel.Function(ipToString,
|
||||
cel.Overload("ip_to_string", []*cel.Type{IPType}, cel.StringType,
|
||||
cel.UnaryBinding(netIPToString)),
|
||||
),
|
||||
cel.Function(isCanonicalFunc,
|
||||
cel.Overload("ip_is_canonical", []*cel.Type{cel.StringType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIPIsCanonical)),
|
||||
),
|
||||
cel.Function(isCIDRFunc,
|
||||
cel.Overload("is_cidr", []*cel.Type{cel.StringType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIsCIDR)),
|
||||
),
|
||||
cel.Function(isGlobalUnicastFunc,
|
||||
cel.MemberOverload("ip_is_global_unicast", []*cel.Type{IPType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIPIsGlobalUnicast)),
|
||||
),
|
||||
cel.Function(isIPFunc,
|
||||
cel.Overload("is_ip", []*cel.Type{cel.StringType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIsIP)),
|
||||
),
|
||||
cel.Function(isLinkLocalMcastFunc,
|
||||
cel.MemberOverload("ip_is_link_local_multicast", []*cel.Type{IPType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIPIsLinkLocalMulticast)),
|
||||
),
|
||||
cel.Function(isLinkLocalUcastFunc,
|
||||
cel.MemberOverload("ip_is_link_local_unicast", []*cel.Type{IPType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIPIsLinkLocalUnicast)),
|
||||
),
|
||||
cel.Function(isLoopbackFunc,
|
||||
cel.MemberOverload("ip_is_loopback", []*cel.Type{IPType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIPIsLoopback)),
|
||||
),
|
||||
cel.Function(isMaskFunc,
|
||||
cel.MemberOverload("cidr_is_mask", []*cel.Type{CIDRType}, cel.BoolType,
|
||||
cel.UnaryBinding(netCIDRIsMask)),
|
||||
),
|
||||
cel.Function(isUnspecifiedFunc,
|
||||
cel.MemberOverload("ip_is_unspecified", []*cel.Type{IPType}, cel.BoolType,
|
||||
cel.UnaryBinding(netIPIsUnspecified)),
|
||||
),
|
||||
cel.Function(maskedFunc,
|
||||
cel.MemberOverload("cidr_masked", []*cel.Type{CIDRType}, CIDRType,
|
||||
cel.UnaryBinding(netCIDRMasked)),
|
||||
),
|
||||
cel.Function(prefixLengthFunc,
|
||||
cel.MemberOverload("cidr_prefix_length", []*cel.Type{CIDRType}, cel.IntType,
|
||||
cel.UnaryBinding(netCIDRPrefixLength)),
|
||||
),
|
||||
cel.ASTValidators(
|
||||
networkFormatValidator{funcName: ipFunc, argNum: 0, check: checkIP},
|
||||
networkFormatValidator{funcName: cidrFunc, argNum: 0, check: checkCIDR},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (*networkLib) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
|
||||
// networkAdapter adapts netip types while preserving existing adapters.
|
||||
type networkAdapter struct {
|
||||
types.Adapter
|
||||
}
|
||||
|
||||
func (a *networkAdapter) NativeToValue(value any) ref.Val {
|
||||
switch v := value.(type) {
|
||||
case netip.Addr:
|
||||
return IP{Addr: v}
|
||||
case netip.Prefix:
|
||||
return CIDR{Prefix: v}
|
||||
}
|
||||
// Delegate to the wrapped adapter (e.g., Protobuf adapter)
|
||||
return a.Adapter.NativeToValue(value)
|
||||
}
|
||||
|
||||
// --- Implementation Logic ---
|
||||
|
||||
func netCIDRContainsCIDR(lhs, rhs ref.Val) ref.Val {
|
||||
parent := lhs.(CIDR)
|
||||
child := rhs.(CIDR)
|
||||
return types.Bool(parent.Prefix.Overlaps(child.Prefix) && parent.Prefix.Bits() <= child.Prefix.Bits())
|
||||
}
|
||||
|
||||
func netCIDRContainsCIDRString(lhs, rhs ref.Val) ref.Val {
|
||||
parent := lhs.(CIDR)
|
||||
s := rhs.(types.String)
|
||||
childPrefix, err := parseCIDR(string(s))
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
return types.Bool(parent.Prefix.Overlaps(childPrefix) && parent.Prefix.Bits() <= childPrefix.Bits())
|
||||
}
|
||||
|
||||
func netCIDRContainsIP(lhs, rhs ref.Val) ref.Val {
|
||||
cidr := lhs.(CIDR)
|
||||
ip := rhs.(IP)
|
||||
return types.Bool(cidr.Prefix.Contains(ip.Addr))
|
||||
}
|
||||
|
||||
func netCIDRContainsIPString(lhs, rhs ref.Val) ref.Val {
|
||||
cidr := lhs.(CIDR)
|
||||
s := rhs.(types.String)
|
||||
addr, err := parseIPAddr(string(s))
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
return types.Bool(cidr.Prefix.Contains(addr))
|
||||
}
|
||||
|
||||
func netCIDRIP(val ref.Val) ref.Val {
|
||||
cidr := val.(CIDR)
|
||||
return IP{Addr: cidr.Prefix.Addr()}
|
||||
}
|
||||
|
||||
func netCIDRMasked(val ref.Val) ref.Val {
|
||||
cidr := val.(CIDR)
|
||||
return CIDR{Prefix: cidr.Prefix.Masked()}
|
||||
}
|
||||
|
||||
func netCIDRPrefixLength(val ref.Val) ref.Val {
|
||||
cidr := val.(CIDR)
|
||||
return types.Int(cidr.Prefix.Bits())
|
||||
}
|
||||
|
||||
func netCIDRString(val ref.Val) ref.Val {
|
||||
s := val.(types.String)
|
||||
str := string(s)
|
||||
prefix, err := parseCIDR(str)
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
return CIDR{Prefix: prefix}
|
||||
}
|
||||
|
||||
func netCIDRToString(val ref.Val) ref.Val {
|
||||
cidr := val.(CIDR)
|
||||
return types.String(cidr.Prefix.String())
|
||||
}
|
||||
|
||||
func netIPFamily(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
if ip.Addr.Is4() {
|
||||
return types.Int(4)
|
||||
}
|
||||
return types.Int(6)
|
||||
}
|
||||
|
||||
func netIPIsCanonical(val ref.Val) ref.Val {
|
||||
s := val.(types.String)
|
||||
str := string(s)
|
||||
addr, err := parseIPAddr(str)
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
return types.Bool(addr.String() == str)
|
||||
}
|
||||
|
||||
func netIPIsGlobalUnicast(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
return types.Bool(ip.Addr.IsGlobalUnicast())
|
||||
}
|
||||
|
||||
func netIPIsLinkLocalMulticast(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
return types.Bool(ip.Addr.IsLinkLocalMulticast())
|
||||
}
|
||||
|
||||
func netIPIsLinkLocalUnicast(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
return types.Bool(ip.Addr.IsLinkLocalUnicast())
|
||||
}
|
||||
|
||||
func netIPIsLoopback(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
return types.Bool(ip.Addr.IsLoopback())
|
||||
}
|
||||
|
||||
func netIPIsUnspecified(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
return types.Bool(ip.Addr.IsUnspecified())
|
||||
}
|
||||
|
||||
func netIPString(val ref.Val) ref.Val {
|
||||
s := val.(types.String)
|
||||
str := string(s)
|
||||
addr, err := parseIPAddr(str)
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
return IP{Addr: addr}
|
||||
}
|
||||
|
||||
func netIPToString(val ref.Val) ref.Val {
|
||||
ip := val.(IP)
|
||||
return types.String(ip.Addr.String())
|
||||
}
|
||||
|
||||
func netIsCIDR(val ref.Val) ref.Val {
|
||||
s := val.(types.String)
|
||||
_, err := parseCIDR(string(s))
|
||||
return types.Bool(err == nil)
|
||||
}
|
||||
|
||||
func netIsIP(val ref.Val) ref.Val {
|
||||
s := val.(types.String)
|
||||
_, err := parseIPAddr(string(s))
|
||||
return types.Bool(err == nil)
|
||||
}
|
||||
|
||||
func netCIDRIsMask(val ref.Val) ref.Val {
|
||||
cidr := val.(CIDR)
|
||||
return types.Bool(cidr.Prefix.Addr() == cidr.Prefix.Masked().Addr())
|
||||
}
|
||||
|
||||
func parseCIDR(raw string) (netip.Prefix, error) {
|
||||
prefix, err := netip.ParsePrefix(raw)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("CIDR %q parse error during conversion from string: %v", raw, err)
|
||||
}
|
||||
if prefix.Addr().Zone() != "" {
|
||||
return netip.Prefix{}, fmt.Errorf("CIDR %q with zone value is not allowed", raw)
|
||||
}
|
||||
if prefix.Addr().Is4In6() {
|
||||
return netip.Prefix{}, fmt.Errorf("IPv4-mapped IPv6 address %q is not allowed", raw)
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
func parseIPAddr(raw string) (netip.Addr, error) {
|
||||
addr, err := netip.ParseAddr(raw)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("IP Address %q parse error during conversion from string: %v", raw, err)
|
||||
}
|
||||
if addr.Zone() != "" {
|
||||
return netip.Addr{}, fmt.Errorf("IP address %q with zone value is not allowed", raw)
|
||||
}
|
||||
if addr.Is4In6() {
|
||||
return netip.Addr{}, fmt.Errorf("IPv4-mapped IPv6 address %q is not allowed", raw)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// --- Opaque Type Wrappers ---
|
||||
|
||||
type IP struct {
|
||||
netip.Addr
|
||||
}
|
||||
|
||||
// ConvertToNative converts the IP value to a native Go type.
|
||||
func (i IP) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
if typeDesc == reflect.TypeFor[netip.Addr]() {
|
||||
return i.Addr, nil
|
||||
}
|
||||
if typeDesc.Kind() == reflect.String {
|
||||
return i.Addr.String(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported type conversion to '%v'", typeDesc)
|
||||
}
|
||||
|
||||
// ConvertToType converts the IP value to a CEL type.
|
||||
func (i IP) ConvertToType(typeValue ref.Type) ref.Val {
|
||||
switch typeValue {
|
||||
case types.StringType:
|
||||
return types.String(i.Addr.String())
|
||||
case IPType:
|
||||
return i
|
||||
case types.TypeType:
|
||||
return IPType
|
||||
}
|
||||
return types.NewErr("type conversion error from '%s' to '%s'", IPType, typeValue)
|
||||
}
|
||||
|
||||
// Equal returns true if this IP is equal to the other ref.Val.
|
||||
func (i IP) Equal(other ref.Val) ref.Val {
|
||||
o, ok := other.(IP)
|
||||
if !ok {
|
||||
return types.False
|
||||
}
|
||||
return types.Bool(i.Addr == o.Addr)
|
||||
}
|
||||
|
||||
// Type returns the CEL type of the IP.
|
||||
func (i IP) Type() ref.Type {
|
||||
return IPType
|
||||
}
|
||||
|
||||
// Value returns the raw Go value (netip.Addr) of the IP.
|
||||
func (i IP) Value() any {
|
||||
return i.Addr
|
||||
}
|
||||
|
||||
type CIDR struct {
|
||||
netip.Prefix
|
||||
}
|
||||
|
||||
// ConvertToNative converts the CIDR value to a native Go type.
|
||||
func (c CIDR) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
if typeDesc == reflect.TypeFor[netip.Prefix]() {
|
||||
return c.Prefix, nil
|
||||
}
|
||||
if typeDesc.Kind() == reflect.String {
|
||||
return c.Prefix.String(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported type conversion to '%v'", typeDesc)
|
||||
}
|
||||
|
||||
// ConvertToType converts the CIDR value to a CEL type.
|
||||
func (c CIDR) ConvertToType(typeValue ref.Type) ref.Val {
|
||||
switch typeValue {
|
||||
case types.StringType:
|
||||
return types.String(c.Prefix.String())
|
||||
case CIDRType:
|
||||
return c
|
||||
case types.TypeType:
|
||||
return CIDRType
|
||||
}
|
||||
return types.NewErr("type conversion error from '%s' to '%s'", CIDRType, typeValue)
|
||||
}
|
||||
|
||||
// Equal returns true if this CIDR is equal to the other ref.Val.
|
||||
func (c CIDR) Equal(other ref.Val) ref.Val {
|
||||
o, ok := other.(CIDR)
|
||||
if !ok {
|
||||
return types.False
|
||||
}
|
||||
return types.Bool(c.Prefix == o.Prefix)
|
||||
}
|
||||
|
||||
// Type returns the CEL type of the CIDR.
|
||||
func (c CIDR) Type() ref.Type {
|
||||
return CIDRType
|
||||
}
|
||||
|
||||
// Value returns the raw Go value (netip.Prefix) of the CIDR.
|
||||
func (c CIDR) Value() any {
|
||||
return c.Prefix
|
||||
}
|
||||
|
||||
// --- Static Validators ---
|
||||
|
||||
type argChecker func(e *cel.Env, call, arg ast.Expr) error
|
||||
|
||||
type networkFormatValidator struct {
|
||||
funcName string
|
||||
argNum int
|
||||
check argChecker
|
||||
}
|
||||
|
||||
func (v networkFormatValidator) Name() string {
|
||||
return fmt.Sprintf("cel.validator.network.%s", v.funcName)
|
||||
}
|
||||
|
||||
func (v networkFormatValidator) Validate(e *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) {
|
||||
root := ast.NavigateAST(a)
|
||||
funcCalls := ast.MatchDescendants(root, ast.FunctionMatcher(v.funcName))
|
||||
for _, call := range funcCalls {
|
||||
callArgs := call.AsCall().Args()
|
||||
if len(callArgs) <= v.argNum {
|
||||
continue
|
||||
}
|
||||
litArg := callArgs[v.argNum]
|
||||
if litArg.Kind() != ast.LiteralKind {
|
||||
continue
|
||||
}
|
||||
if err := v.check(e, call, litArg); err != nil {
|
||||
iss.ReportErrorAtID(litArg.ID(), "invalid %s argument: %v", v.funcName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkIP(e *cel.Env, call, arg ast.Expr) error {
|
||||
pattern := arg.AsLiteral().Value().(string)
|
||||
_, err := parseIPAddr(pattern)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkCIDR(e *cel.Env, call, arg ast.Expr) error {
|
||||
pattern := arg.AsLiteral().Value().(string)
|
||||
_, err := parseCIDR(pattern)
|
||||
return err
|
||||
}
|
||||
+18
-10
@@ -663,15 +663,19 @@ func indexOf(str, substr string) (int64, error) {
|
||||
}
|
||||
|
||||
func indexOfOffset(str, substr string, offset int64) (int64, error) {
|
||||
if substr == "" {
|
||||
return offset, nil
|
||||
}
|
||||
off := int(offset)
|
||||
runes := []rune(str)
|
||||
subrunes := []rune(substr)
|
||||
if off < 0 {
|
||||
return -1, fmt.Errorf("index out of range: %d", off)
|
||||
}
|
||||
runes := []rune(str)
|
||||
if substr == "" {
|
||||
// The empty string matches at the search offset, clamped to the end of the string.
|
||||
if off > len(runes) {
|
||||
return int64(len(runes)), nil
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
subrunes := []rune(substr)
|
||||
// If the offset exceeds the length, return -1 rather than error.
|
||||
if off >= len(runes) {
|
||||
return -1, nil
|
||||
@@ -704,15 +708,19 @@ func lastIndexOf(str, substr string) (int64, error) {
|
||||
}
|
||||
|
||||
func lastIndexOfOffset(str, substr string, offset int64) (int64, error) {
|
||||
if substr == "" {
|
||||
return offset, nil
|
||||
}
|
||||
off := int(offset)
|
||||
runes := []rune(str)
|
||||
subrunes := []rune(substr)
|
||||
if off < 0 {
|
||||
return -1, fmt.Errorf("index out of range: %d", off)
|
||||
}
|
||||
runes := []rune(str)
|
||||
if substr == "" {
|
||||
// The empty string matches at the search offset, clamped to the end of the string.
|
||||
if off > len(runes) {
|
||||
return int64(len(runes)), nil
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
subrunes := []rune(substr)
|
||||
// If the offset is far greater than the length return -1
|
||||
if off >= len(runes) {
|
||||
return -1, nil
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ go_library(
|
||||
"decorators.go",
|
||||
"dispatcher.go",
|
||||
"evalstate.go",
|
||||
"frame.go",
|
||||
"interpretable.go",
|
||||
"interpreter.go",
|
||||
"optimizations.go",
|
||||
@@ -47,6 +48,7 @@ go_test(
|
||||
"activation_test.go",
|
||||
"attribute_patterns_test.go",
|
||||
"attributes_test.go",
|
||||
"frame_test.go",
|
||||
"interpreter_test.go",
|
||||
"prune_test.go",
|
||||
"runtimecost_test.go",
|
||||
|
||||
+22
-3
@@ -110,8 +110,9 @@ func (a *mapActivation) ResolveName(name string) (any, bool) {
|
||||
// hierarchicalActivation which implements Activation and contains a parent and
|
||||
// child activation.
|
||||
type hierarchicalActivation struct {
|
||||
parent Activation
|
||||
child Activation
|
||||
parent Activation
|
||||
child Activation
|
||||
poolAllocated bool
|
||||
}
|
||||
|
||||
// Parent implements the Activation interface method.
|
||||
@@ -127,10 +128,28 @@ func (a *hierarchicalActivation) ResolveName(name string) (any, bool) {
|
||||
return a.parent.ResolveName(name)
|
||||
}
|
||||
|
||||
// Unwrap returns the parent activation, stripping the local child scope.
|
||||
// This allows global disambiguation to skip past locally introduced variables.
|
||||
func (a *hierarchicalActivation) Unwrap() Activation {
|
||||
return a.parent
|
||||
}
|
||||
|
||||
// AsPartialActivation checks the child first via direct type assertion (to
|
||||
// avoid recursion through the folder → frame → hierarchicalActivation cycle),
|
||||
// then walks the parent hierarchy via the free function.
|
||||
func (a *hierarchicalActivation) AsPartialActivation() (PartialActivation, bool) {
|
||||
if pv, ok := a.child.(partialActivationConverter); ok {
|
||||
if p, ok := pv.AsPartialActivation(); ok {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return AsPartialActivation(a.parent)
|
||||
}
|
||||
|
||||
// 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}
|
||||
return &hierarchicalActivation{parent: parent, child: child, poolAllocated: false}
|
||||
}
|
||||
|
||||
// NewPartialActivation returns an Activation which contains a list of AttributePattern values
|
||||
|
||||
+15
-11
@@ -190,7 +190,7 @@ func (r *attrFactory) AbsoluteAttribute(id int64, names ...string) NamespacedAtt
|
||||
func (r *attrFactory) ConditionalAttribute(id int64, expr Interpretable, t, f Attribute) Attribute {
|
||||
return &conditionalAttribute{
|
||||
id: id,
|
||||
expr: expr,
|
||||
expr: adaptToV2(expr),
|
||||
truthy: t,
|
||||
falsy: f,
|
||||
adapter: r.adapter,
|
||||
@@ -225,7 +225,7 @@ func (r *attrFactory) MaybeAttribute(id int64, name string) Attribute {
|
||||
func (r *attrFactory) RelativeAttribute(id int64, operand Interpretable) Attribute {
|
||||
return &relativeAttribute{
|
||||
id: id,
|
||||
operand: operand,
|
||||
operand: adaptToV2(operand),
|
||||
qualifiers: []Qualifier{},
|
||||
adapter: r.adapter,
|
||||
fac: r,
|
||||
@@ -384,7 +384,7 @@ func (a *absoluteAttribute) Resolve(vars Activation) (any, error) {
|
||||
|
||||
type conditionalAttribute struct {
|
||||
id int64
|
||||
expr Interpretable
|
||||
expr InterpretableV2
|
||||
truthy Attribute
|
||||
falsy Attribute
|
||||
adapter types.Adapter
|
||||
@@ -571,7 +571,7 @@ func (a *maybeAttribute) String() string {
|
||||
|
||||
type relativeAttribute struct {
|
||||
id int64
|
||||
operand Interpretable
|
||||
operand InterpretableV2
|
||||
qualifiers []Qualifier
|
||||
adapter types.Adapter
|
||||
fac AttributeFactory
|
||||
@@ -964,9 +964,11 @@ func (q *intQualifier) qualifyInternal(vars Activation, obj any, presenceTest, p
|
||||
}
|
||||
case map[int32]any:
|
||||
isMap = true
|
||||
obj, isKey := o[int32(i)]
|
||||
if isKey {
|
||||
return obj, true, nil
|
||||
if i32 := int32(i); int64(i32) == i {
|
||||
obj, isKey := o[i32]
|
||||
if isKey {
|
||||
return obj, true, nil
|
||||
}
|
||||
}
|
||||
case map[int64]any:
|
||||
isMap = true
|
||||
@@ -1089,9 +1091,11 @@ func (q *uintQualifier) qualifyInternal(vars Activation, obj any, presenceTest,
|
||||
return obj, true, nil
|
||||
}
|
||||
case map[uint32]any:
|
||||
obj, isKey := o[uint32(u)]
|
||||
if isKey {
|
||||
return obj, true, nil
|
||||
if u32 := uint32(u); uint64(u32) == u {
|
||||
obj, isKey := o[u32]
|
||||
if isKey {
|
||||
return obj, true, nil
|
||||
}
|
||||
}
|
||||
case map[uint64]any:
|
||||
obj, isKey := o[u]
|
||||
@@ -1301,7 +1305,7 @@ func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, boo
|
||||
if !optObj.HasValue() {
|
||||
return optObj, false, nil
|
||||
}
|
||||
obj = optObj.GetValue().Value()
|
||||
obj = optObj.GetValue()
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
+20
-16
@@ -25,9 +25,13 @@ import (
|
||||
// Interpretable expression nodes at construction time.
|
||||
type InterpretableDecorator func(Interpretable) (Interpretable, error)
|
||||
|
||||
// InterpretableDecoratorV2 is a functional interface for decorating or replacing
|
||||
// InterpretableV2 expression nodes at construction time.
|
||||
type InterpretableDecoratorV2 func(InterpretableV2) (InterpretableV2, error)
|
||||
|
||||
// decObserveEval records evaluation state into an EvalState object.
|
||||
func decObserveEval(observer EvalObserver) InterpretableDecorator {
|
||||
return func(i Interpretable) (Interpretable, error) {
|
||||
func decObserveEval(observer EvalObserver) InterpretableDecoratorV2 {
|
||||
return func(i InterpretableV2) (InterpretableV2, error) {
|
||||
switch inst := i.(type) {
|
||||
case *evalWatch, *evalWatchAttr, *evalWatchConst, *evalWatchConstructor:
|
||||
// these instruction are already watching, return straight-away.
|
||||
@@ -49,8 +53,8 @@ func decObserveEval(observer EvalObserver) InterpretableDecorator {
|
||||
}, nil
|
||||
default:
|
||||
return &evalWatch{
|
||||
Interpretable: i,
|
||||
observer: observer,
|
||||
InterpretableV2: i,
|
||||
observer: observer,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -58,8 +62,8 @@ func decObserveEval(observer EvalObserver) InterpretableDecorator {
|
||||
|
||||
// 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) {
|
||||
func decInterruptFolds() InterpretableDecoratorV2 {
|
||||
return func(i InterpretableV2) (InterpretableV2, error) {
|
||||
fold, ok := i.(*evalFold)
|
||||
if !ok {
|
||||
return i, nil
|
||||
@@ -70,8 +74,8 @@ func decInterruptFolds() InterpretableDecorator {
|
||||
}
|
||||
|
||||
// decDisableShortcircuits ensures that all branches of an expression will be evaluated, no short-circuiting.
|
||||
func decDisableShortcircuits() InterpretableDecorator {
|
||||
return func(i Interpretable) (Interpretable, error) {
|
||||
func decDisableShortcircuits() InterpretableDecoratorV2 {
|
||||
return func(i InterpretableV2) (InterpretableV2, error) {
|
||||
switch expr := i.(type) {
|
||||
case *evalOr:
|
||||
return &evalExhaustiveOr{
|
||||
@@ -104,8 +108,8 @@ func decDisableShortcircuits() InterpretableDecorator {
|
||||
// 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) {
|
||||
func decOptimize() InterpretableDecoratorV2 {
|
||||
return func(i InterpretableV2) (InterpretableV2, error) {
|
||||
switch inst := i.(type) {
|
||||
case *evalList:
|
||||
return maybeBuildListLiteral(i, inst)
|
||||
@@ -124,7 +128,7 @@ func decOptimize() InterpretableDecorator {
|
||||
}
|
||||
|
||||
// decRegexOptimizer compiles regex pattern string constants.
|
||||
func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDecorator {
|
||||
func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDecoratorV2 {
|
||||
functionMatchMap := make(map[string]*RegexOptimization)
|
||||
overloadMatchMap := make(map[string]*RegexOptimization)
|
||||
for _, m := range regexOptimizations {
|
||||
@@ -134,7 +138,7 @@ func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDe
|
||||
}
|
||||
}
|
||||
|
||||
return func(i Interpretable) (Interpretable, error) {
|
||||
return func(i InterpretableV2) (InterpretableV2, error) {
|
||||
call, ok := i.(InterpretableCall)
|
||||
if !ok {
|
||||
return i, nil
|
||||
@@ -165,7 +169,7 @@ func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDe
|
||||
}
|
||||
}
|
||||
|
||||
func maybeOptimizeConstUnary(i Interpretable, call InterpretableCall) (Interpretable, error) {
|
||||
func maybeOptimizeConstUnary(i InterpretableV2, call InterpretableCall) (InterpretableV2, error) {
|
||||
args := call.Args()
|
||||
if len(args) != 1 {
|
||||
return i, nil
|
||||
@@ -181,7 +185,7 @@ func maybeOptimizeConstUnary(i Interpretable, call InterpretableCall) (Interpret
|
||||
return NewConstValue(call.ID(), val), nil
|
||||
}
|
||||
|
||||
func maybeBuildListLiteral(i Interpretable, l *evalList) (Interpretable, error) {
|
||||
func maybeBuildListLiteral(i InterpretableV2, l *evalList) (InterpretableV2, error) {
|
||||
for _, elem := range l.elems {
|
||||
_, isConst := elem.(InterpretableConst)
|
||||
if !isConst {
|
||||
@@ -191,7 +195,7 @@ func maybeBuildListLiteral(i Interpretable, l *evalList) (Interpretable, error)
|
||||
return NewConstValue(l.ID(), l.Eval(EmptyActivation())), nil
|
||||
}
|
||||
|
||||
func maybeBuildMapLiteral(i Interpretable, mp *evalMap) (Interpretable, error) {
|
||||
func maybeBuildMapLiteral(i InterpretableV2, mp *evalMap) (InterpretableV2, error) {
|
||||
for idx, key := range mp.keys {
|
||||
_, isConst := key.(InterpretableConst)
|
||||
if !isConst {
|
||||
@@ -209,7 +213,7 @@ func maybeBuildMapLiteral(i Interpretable, mp *evalMap) (Interpretable, error) {
|
||||
// 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) {
|
||||
func maybeOptimizeSetMembership(i InterpretableV2, inlist InterpretableCall) (InterpretableV2, error) {
|
||||
args := inlist.Args()
|
||||
lhs := args[0]
|
||||
rhs := args[1]
|
||||
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
// Copyright 2026 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
// evalContext contains the stateful information needed for a single evaluation.
|
||||
//
|
||||
// This state is shared across all frames within a single evaluation, including
|
||||
// child frames created for comprehension blocks.
|
||||
type evalContext struct {
|
||||
// interrupt exposes a callback channel for cancellation.
|
||||
interrupt <-chan struct{}
|
||||
|
||||
// interruptCheckCount is the number of times the interrupt channel has been checked.
|
||||
interruptCheckCount atomic.Uint64
|
||||
|
||||
// interruptCheckFrequency is the frequency at which the interrupt channel is checked.
|
||||
interruptCheckFrequency uint
|
||||
|
||||
// interrupted indicates whether the evaluation has been interrupted.
|
||||
interrupted atomic.Bool
|
||||
|
||||
// state provides the context for tracking the evaluation state.
|
||||
state EvalState
|
||||
|
||||
// costs provides the context for tracking the evaluation costs.
|
||||
costs *CostTracker
|
||||
|
||||
// ctx is the context for async call implementations to use.
|
||||
ctx context.Context
|
||||
|
||||
// cancel cancels the context when the evaluation is finished.
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// ExecutionFrame provides the context for a single evaluation of an expression.
|
||||
//
|
||||
// The execution frame must not be stored in any fashion as its lifecycle is completely
|
||||
// controlled by the CEL evaluation process.
|
||||
type ExecutionFrame struct {
|
||||
// Activation provides the context for resolving variables by name.
|
||||
Activation
|
||||
|
||||
// parent provides the context for parent scopes (used for comprehension iterators).
|
||||
parent *ExecutionFrame
|
||||
|
||||
// ctx provides the shared evaluation state across frames.
|
||||
ctx *evalContext
|
||||
}
|
||||
|
||||
// NewExecutionFrame creates a new execution frame from the pool.
|
||||
func NewExecutionFrame(input any) (*ExecutionFrame, error) {
|
||||
f := frameStack.Get().(*ExecutionFrame)
|
||||
switch v := input.(type) {
|
||||
case Activation:
|
||||
f.Activation = v
|
||||
case map[string]any:
|
||||
f.Activation = activationInput.create(v)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// SetContext sets the context for the execution frame.
|
||||
func (f *ExecutionFrame) SetContext(ctx context.Context, interruptCheckFrequency uint) error {
|
||||
if f.parent != nil {
|
||||
return errors.New("SetContext() called on child frame")
|
||||
}
|
||||
if f.ctx != nil {
|
||||
return errors.New("SetContext() called more than once")
|
||||
}
|
||||
f.ctx = evalContextPool.Get().(*evalContext)
|
||||
f.ctx.ctx, f.ctx.cancel = context.WithCancel(ctx)
|
||||
f.ctx.interrupt = ctx.Done()
|
||||
f.ctx.interruptCheckFrequency = interruptCheckFrequency
|
||||
f.ctx.interruptCheckCount.Store(0)
|
||||
f.ctx.interrupted.Store(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases the resources held by the execution frame and returns it to the pool.
|
||||
func (f *ExecutionFrame) Close() {
|
||||
if f.parent == nil && f.ctx != nil {
|
||||
if f.ctx.cancel != nil {
|
||||
f.ctx.cancel()
|
||||
f.ctx.cancel = nil
|
||||
}
|
||||
f.ctx.ctx = nil
|
||||
f.ctx.interrupt = nil
|
||||
f.ctx.state = nil
|
||||
f.ctx.costs = nil
|
||||
f.ctx.interrupted.Store(false)
|
||||
f.ctx.interruptCheckCount.Store(0)
|
||||
f.ctx.interruptCheckFrequency = 0
|
||||
evalContextPool.Put(f.ctx)
|
||||
}
|
||||
f.ctx = nil
|
||||
f.parent = nil
|
||||
switch a := f.Activation.(type) {
|
||||
case *hierarchicalActivation:
|
||||
if child, ok := a.child.(*inputActivation); ok {
|
||||
activationInput.release(child)
|
||||
}
|
||||
activationStack.release(a)
|
||||
case *inputActivation:
|
||||
activationInput.release(a)
|
||||
}
|
||||
f.Activation = nil
|
||||
frameStack.Put(f)
|
||||
}
|
||||
|
||||
// Push pushes the given activation onto the activation stack and returns the new frame.
|
||||
//
|
||||
// This operation is internal to the interpreter and is used to handle comprehension
|
||||
// scoping. The child frame inherits the shared evalContext from the parent.
|
||||
func (f *ExecutionFrame) Push(activation Activation) *ExecutionFrame {
|
||||
child := frameStack.Get().(*ExecutionFrame)
|
||||
child.parent = f
|
||||
child.ctx = f.ctx
|
||||
child.Activation = activationStack.create(f.Activation, activation)
|
||||
return child
|
||||
}
|
||||
|
||||
// Pop returns the parent frame, releasing the current frame back to the pool.
|
||||
func (f *ExecutionFrame) Pop() *ExecutionFrame {
|
||||
if f.parent == nil {
|
||||
return f
|
||||
}
|
||||
parent := f.parent
|
||||
activationStack.release(f.Activation)
|
||||
f.Activation = nil
|
||||
f.parent = nil
|
||||
f.ctx = nil
|
||||
frameStack.Put(f)
|
||||
return parent
|
||||
}
|
||||
|
||||
// ResolveName implements the Activation interface by proxying to the internal activation.
|
||||
func (f *ExecutionFrame) ResolveName(name string) (any, bool) {
|
||||
return f.Activation.ResolveName(name)
|
||||
}
|
||||
|
||||
// Parent implements the Activation interface by proxying to the internal activation.
|
||||
func (f *ExecutionFrame) Parent() Activation {
|
||||
return f.Activation.Parent()
|
||||
}
|
||||
|
||||
// AsPartialActivation implements the PartialActivation interface by proxying to the internal activation.
|
||||
func (f *ExecutionFrame) AsPartialActivation() (PartialActivation, bool) {
|
||||
return AsPartialActivation(f.Activation)
|
||||
}
|
||||
|
||||
// Unwrap returns the internal activation.
|
||||
func (f *ExecutionFrame) Unwrap() Activation {
|
||||
return f.Activation
|
||||
}
|
||||
|
||||
// CheckInterrupt returns whether the evaluation has been interrupted.
|
||||
func (f *ExecutionFrame) CheckInterrupt() bool {
|
||||
if f.ctx == nil {
|
||||
return false
|
||||
}
|
||||
if f.ctx.interrupted.Load() {
|
||||
return true
|
||||
}
|
||||
count := f.ctx.interruptCheckCount.Add(1)
|
||||
if f.ctx.interruptCheckFrequency > 0 && count%uint64(f.ctx.interruptCheckFrequency) == 0 {
|
||||
select {
|
||||
case <-f.ctx.interrupt:
|
||||
f.ctx.interrupted.Store(true)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// frameStack provides a synchronized pool of ExecutionFrames.
|
||||
var frameStack = &sync.Pool{
|
||||
New: func() any {
|
||||
return &ExecutionFrame{}
|
||||
},
|
||||
}
|
||||
|
||||
// evalContextPool provides a synchronized pool of evalContexts.
|
||||
var evalContextPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &evalContext{}
|
||||
},
|
||||
}
|
||||
|
||||
type activationStackPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
func (pool *activationStackPool) create(parent, child Activation) Activation {
|
||||
h := pool.Get().(*hierarchicalActivation)
|
||||
h.child = child
|
||||
h.parent = parent
|
||||
h.poolAllocated = true
|
||||
return h
|
||||
}
|
||||
|
||||
func (pool *activationStackPool) release(activation Activation) {
|
||||
h, ok := activation.(*hierarchicalActivation)
|
||||
if !ok || !h.poolAllocated {
|
||||
return
|
||||
}
|
||||
h.parent = nil
|
||||
h.child = nil
|
||||
pool.Pool.Put(h)
|
||||
}
|
||||
|
||||
func newActivationStackPool() *activationStackPool {
|
||||
return &activationStackPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() any {
|
||||
return &hierarchicalActivation{}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type inputActivation struct {
|
||||
vars map[string]any
|
||||
lazyVars map[string]any
|
||||
}
|
||||
|
||||
// ResolveName looks up the value of the input variable name, if found.
|
||||
//
|
||||
// Lazy bindings may be supplied within the map-based input in either of the following forms:
|
||||
// - func() any
|
||||
// - func() ref.Val
|
||||
//
|
||||
// The lazy binding will only be invoked once per evaluation.
|
||||
//
|
||||
// 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 (a *inputActivation) ResolveName(name string) (any, bool) {
|
||||
v, found := a.vars[name]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
switch obj := v.(type) {
|
||||
case func() ref.Val:
|
||||
if resolved, found := a.lazyVars[name]; found {
|
||||
return resolved, true
|
||||
}
|
||||
lazy := obj()
|
||||
a.lazyVars[name] = lazy
|
||||
return lazy, true
|
||||
case func() any:
|
||||
if resolved, found := a.lazyVars[name]; found {
|
||||
return resolved, true
|
||||
}
|
||||
lazy := obj()
|
||||
a.lazyVars[name] = lazy
|
||||
return lazy, true
|
||||
default:
|
||||
return obj, true
|
||||
}
|
||||
}
|
||||
|
||||
// Parent implements the Activation interface
|
||||
func (a *inputActivation) Parent() Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newActivationInputPool() *activationInputPool {
|
||||
return &activationInputPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() any {
|
||||
return &inputActivation{
|
||||
lazyVars: make(map[string]any),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type activationInputPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
// create initializes a pooled Activation object with the map input.
|
||||
func (p *activationInputPool) create(vars map[string]any) *inputActivation {
|
||||
a := p.Pool.Get().(*inputActivation)
|
||||
a.vars = vars
|
||||
return a
|
||||
}
|
||||
|
||||
func (p *activationInputPool) release(value any) {
|
||||
a := value.(*inputActivation)
|
||||
for k := range a.lazyVars {
|
||||
delete(a.lazyVars, k)
|
||||
}
|
||||
a.vars = nil
|
||||
p.Pool.Put(a)
|
||||
}
|
||||
|
||||
var (
|
||||
activationStack = newActivationStackPool()
|
||||
activationInput = newActivationInputPool()
|
||||
)
|
||||
+346
-157
File diff suppressed because it is too large
Load Diff
+44
-76
@@ -29,11 +29,11 @@ import (
|
||||
// PlannerOption configures the program plan options during interpretable setup.
|
||||
type PlannerOption func(*planner) (*planner, error)
|
||||
|
||||
// Interpreter generates a new Interpretable from a checked or unchecked expression.
|
||||
// Interpreter generates a new InterpretableV2 from a checked or unchecked expression.
|
||||
type Interpreter interface {
|
||||
// NewInterpretable creates an Interpretable from a checked expression and an
|
||||
// NewInterpretable creates an InterpretableV2 from a checked expression and an
|
||||
// optional list of PlannerOption values.
|
||||
NewInterpretable(exprAST *ast.AST, opts ...PlannerOption) (Interpretable, error)
|
||||
NewInterpretable(exprAST *ast.AST, opts ...PlannerOption) (InterpretableV2, error)
|
||||
}
|
||||
|
||||
// EvalObserver is a functional interface that accepts an expression id and an observed value.
|
||||
@@ -43,16 +43,16 @@ type EvalObserver func(vars Activation, id int64, programStep any, value ref.Val
|
||||
|
||||
// StatefulObserver observes evaluation while tracking or utilizing stateful behavior.
|
||||
type StatefulObserver interface {
|
||||
// InitState configures stateful metadata on the activation.
|
||||
InitState(Activation) (Activation, error)
|
||||
// InitState configures stateful metadata on the execution frame.
|
||||
InitState(*ExecutionFrame) (any, error)
|
||||
|
||||
// GetState retrieves the stateful metadata from the activation.
|
||||
GetState(Activation) any
|
||||
// GetState retrieves the stateful metadata from the execution frame.
|
||||
GetState(*ExecutionFrame) any
|
||||
|
||||
// Observe passes the activation and relevant evaluation metadata to the observer.
|
||||
// The observe method is expected to do the equivalent of GetState(vars) in order
|
||||
// The observe method is expected to do the equivalent of GetState(AsFrame(activation))
|
||||
// to find the metadata that needs to be updated upon invocation.
|
||||
Observe(vars Activation, id int64, programStep any, value ref.Val)
|
||||
Observe(Activation, int64, any, ref.Val)
|
||||
}
|
||||
|
||||
// EvalCancelledError represents a cancelled program evaluation operation.
|
||||
@@ -106,37 +106,6 @@ func EvalStateObserver(opts ...evalStateOption) PlannerOption {
|
||||
}
|
||||
}
|
||||
|
||||
// evalStateConverter identifies an object which is convertible to an EvalState instance.
|
||||
type evalStateConverter interface {
|
||||
asEvalState() EvalState
|
||||
}
|
||||
|
||||
// evalStateActivation hides state in the Activation in a manner not accessible to expressions.
|
||||
type evalStateActivation struct {
|
||||
vars Activation
|
||||
state EvalState
|
||||
}
|
||||
|
||||
// ResolveName proxies variable lookups to the backing activation.
|
||||
func (esa evalStateActivation) ResolveName(name string) (any, bool) {
|
||||
return esa.vars.ResolveName(name)
|
||||
}
|
||||
|
||||
// Parent proxies parent lookups to the backing activation.
|
||||
func (esa evalStateActivation) Parent() Activation {
|
||||
return esa.vars
|
||||
}
|
||||
|
||||
// AsPartialActivation supports conversion to a partial activation in order to detect unknown attributes.
|
||||
func (esa evalStateActivation) AsPartialActivation() (PartialActivation, bool) {
|
||||
return AsPartialActivation(esa.vars)
|
||||
}
|
||||
|
||||
// asEvalState implements the evalStateConverter method.
|
||||
func (esa evalStateActivation) asEvalState() EvalState {
|
||||
return esa.state
|
||||
}
|
||||
|
||||
// activationWrapper identifies an object carrying local variables which should not be exposed to the user
|
||||
// Activations used for such purposes can be unwrapped to return the activation which omits local state.
|
||||
type activationWrapper interface {
|
||||
@@ -144,57 +113,56 @@ type activationWrapper interface {
|
||||
Unwrap() Activation
|
||||
}
|
||||
|
||||
// asEvalState walks the Activation hierarchy and returns the first EvalState found, if present.
|
||||
func asEvalState(vars Activation) (EvalState, bool) {
|
||||
if conv, ok := vars.(evalStateConverter); ok {
|
||||
return conv.asEvalState(), true
|
||||
}
|
||||
// Check if the current activation wraps another activation. This is used to support
|
||||
// wrappers such as the @block() activation which may be composed of a dynamicSlotActivation or a
|
||||
// constantSlotActivation. In this case, the underlying activation is the portion which interacts
|
||||
// with the EvalState.
|
||||
if wrapper, ok := vars.(activationWrapper); ok {
|
||||
unwrapped := wrapper.Unwrap()
|
||||
// Recursively call asEvalState on the unwrapped activation. This will check the unwrapped value and its parents.
|
||||
return asEvalState(unwrapped)
|
||||
}
|
||||
if vars.Parent() != nil {
|
||||
return asEvalState(vars.Parent())
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// evalStateFactory holds a reference to a factory function that produces an EvalState instance.
|
||||
type evalStateFactory struct {
|
||||
factory func() EvalState
|
||||
}
|
||||
|
||||
// InitState produces an EvalState instance and bundles it into the Activation in a way which is
|
||||
// InitState produces an EvalState instance and bundles it into the ExecutionFrame in a way which is
|
||||
// not visible to expression evaluation.
|
||||
func (et *evalStateFactory) InitState(vars Activation) (Activation, error) {
|
||||
func (et *evalStateFactory) InitState(frame *ExecutionFrame) (any, error) {
|
||||
state := et.factory()
|
||||
return evalStateActivation{vars: vars, state: state}, nil
|
||||
if frame.ctx == nil {
|
||||
frame.ctx = evalContextPool.Get().(*evalContext)
|
||||
}
|
||||
frame.ctx.state = state
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// GetState extracts the EvalState from the Activation.
|
||||
func (et *evalStateFactory) GetState(vars Activation) any {
|
||||
if state, found := asEvalState(vars); found {
|
||||
return state
|
||||
func (et *evalStateFactory) GetState(frame *ExecutionFrame) any {
|
||||
if frame.ctx == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return frame.ctx.state
|
||||
}
|
||||
|
||||
// Observe records the evaluation state for a given expression node and program step.
|
||||
func (et *evalStateFactory) Observe(vars Activation, id int64, programStep any, val ref.Val) {
|
||||
state, found := asEvalState(vars)
|
||||
if !found {
|
||||
frame := AsFrame(vars)
|
||||
if frame.ctx == nil || frame.ctx.state == nil {
|
||||
return
|
||||
}
|
||||
state.SetValue(id, val)
|
||||
frame.ctx.state.SetValue(id, val)
|
||||
}
|
||||
|
||||
// CustomDecorator configures a custom interpretable decorator for the program.
|
||||
func CustomDecorator(dec InterpretableDecorator) PlannerOption {
|
||||
return func(p *planner) (*planner, error) {
|
||||
dec2 := func(i InterpretableV2) (InterpretableV2, error) {
|
||||
legacy, err := dec(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return adaptToV2(legacy), nil
|
||||
}
|
||||
p.decorators = append(p.decorators, dec2)
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CustomDecoratorV2 configures a custom V2 interpretable decorator for the program.
|
||||
func CustomDecoratorV2(dec InterpretableDecoratorV2) PlannerOption {
|
||||
return func(p *planner) (*planner, error) {
|
||||
p.decorators = append(p.decorators, dec)
|
||||
return p, nil
|
||||
@@ -207,7 +175,7 @@ func CustomDecorator(dec InterpretableDecorator) PlannerOption {
|
||||
// provided to the decorator. This decorator is not thread-safe, and the EvalState
|
||||
// must be reset between Eval() calls.
|
||||
func ExhaustiveEval() PlannerOption {
|
||||
return CustomDecorator(decDisableShortcircuits())
|
||||
return CustomDecoratorV2(decDisableShortcircuits())
|
||||
}
|
||||
|
||||
// InterruptableEval annotates comprehension loops with information that indicates they
|
||||
@@ -216,13 +184,13 @@ func ExhaustiveEval() PlannerOption {
|
||||
// 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() PlannerOption {
|
||||
return CustomDecorator(decInterruptFolds())
|
||||
return CustomDecoratorV2(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() PlannerOption {
|
||||
return CustomDecorator(decOptimize())
|
||||
return CustomDecoratorV2(decOptimize())
|
||||
}
|
||||
|
||||
// RegexOptimization provides a way to replace an InterpretableCall for a regex function when the
|
||||
@@ -247,7 +215,7 @@ type RegexOptimization struct {
|
||||
// CompileRegexConstants compiles regex pattern string constants at program creation time and reports any regex pattern
|
||||
// compile errors.
|
||||
func CompileRegexConstants(regexOptimizations ...*RegexOptimization) PlannerOption {
|
||||
return CustomDecorator(decRegexOptimizer(regexOptimizations...))
|
||||
return CustomDecoratorV2(decRegexOptimizer(regexOptimizations...))
|
||||
}
|
||||
|
||||
type exprInterpreter struct {
|
||||
@@ -273,10 +241,10 @@ func NewInterpreter(dispatcher Dispatcher,
|
||||
attrFactory: attrFactory}
|
||||
}
|
||||
|
||||
// NewIntepretable implements the Interpreter interface method.
|
||||
// NewInterpretable implements the Interpreter interface method.
|
||||
func (i *exprInterpreter) NewInterpretable(
|
||||
checked *ast.AST,
|
||||
opts ...PlannerOption) (Interpretable, error) {
|
||||
opts ...PlannerOption) (InterpretableV2, error) {
|
||||
p := newPlanner(i.dispatcher, i.provider, i.adapter, i.attrFactory, i.container, checked)
|
||||
var err error
|
||||
for _, o := range opts {
|
||||
|
||||
+31
-31
@@ -43,7 +43,7 @@ func newPlanner(disp Dispatcher,
|
||||
container: cont,
|
||||
refMap: exprAST.ReferenceMap(),
|
||||
typeMap: exprAST.TypeMap(),
|
||||
decorators: make([]InterpretableDecorator, 0),
|
||||
decorators: make([]InterpretableDecoratorV2, 0),
|
||||
observers: make([]StatefulObserver, 0),
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ type planner struct {
|
||||
container *containers.Container
|
||||
refMap map[int64]*ast.ReferenceInfo
|
||||
typeMap map[int64]*types.Type
|
||||
decorators []InterpretableDecorator
|
||||
decorators []InterpretableDecoratorV2
|
||||
observers []StatefulObserver
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ type planBuilder struct {
|
||||
// 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) {
|
||||
func (p *planner) Plan(expr ast.Expr) (InterpretableV2, error) {
|
||||
pb := &planBuilder{planner: p, localVars: make(map[string]int)}
|
||||
i, err := pb.plan(expr)
|
||||
if err != nil {
|
||||
@@ -81,10 +81,10 @@ func (p *planner) Plan(expr ast.Expr) (Interpretable, error) {
|
||||
if len(p.observers) == 0 {
|
||||
return i, nil
|
||||
}
|
||||
return &ObservableInterpretable{Interpretable: i, observers: p.observers}, nil
|
||||
return &ObservableInterpretable{InterpretableV2: i, observers: p.observers}, nil
|
||||
}
|
||||
|
||||
func (p *planBuilder) plan(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) plan(expr ast.Expr) (InterpretableV2, error) {
|
||||
switch expr.Kind() {
|
||||
case ast.CallKind:
|
||||
return p.decorate(p.planCall(expr))
|
||||
@@ -109,7 +109,7 @@ func (p *planBuilder) plan(expr ast.Expr) (Interpretable, error) {
|
||||
// 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 *planBuilder) decorate(i Interpretable, err error) (Interpretable, error) {
|
||||
func (p *planBuilder) decorate(i InterpretableV2, err error) (InterpretableV2, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (p *planBuilder) decorate(i Interpretable, err error) (Interpretable, error
|
||||
}
|
||||
|
||||
// planIdent creates an Interpretable that resolves an identifier from an Activation.
|
||||
func (p *planBuilder) planIdent(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planIdent(expr ast.Expr) (InterpretableV2, error) {
|
||||
// Establish whether the identifier is in the reference map.
|
||||
if identRef, found := p.refMap[expr.ID()]; found {
|
||||
return p.planCheckedIdent(expr.ID(), identRef)
|
||||
@@ -142,7 +142,7 @@ func (p *planBuilder) planIdent(expr ast.Expr) (Interpretable, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *planBuilder) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (Interpretable, error) {
|
||||
func (p *planBuilder) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (InterpretableV2, error) {
|
||||
// Plan a constant reference if this is the case for this simple identifier.
|
||||
if identRef.Value != nil {
|
||||
return NewConstValue(id, identRef.Value), nil
|
||||
@@ -171,7 +171,7 @@ func (p *planBuilder) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (I
|
||||
// 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 *planBuilder) planSelect(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planSelect(expr ast.Expr) (InterpretableV2, 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 {
|
||||
@@ -227,7 +227,7 @@ func (p *planBuilder) planSelect(expr ast.Expr) (Interpretable, error) {
|
||||
// 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 *planBuilder) planCall(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planCall(expr ast.Expr) (InterpretableV2, error) {
|
||||
call := expr.AsCall()
|
||||
target, fnName, oName := p.resolveFunction(expr)
|
||||
argCount := len(call.Args())
|
||||
@@ -237,7 +237,7 @@ func (p *planBuilder) planCall(expr ast.Expr) (Interpretable, error) {
|
||||
offset++
|
||||
}
|
||||
|
||||
args := make([]Interpretable, argCount)
|
||||
args := make([]InterpretableV2, argCount)
|
||||
if target != nil {
|
||||
arg, err := p.plan(target)
|
||||
if err != nil {
|
||||
@@ -307,7 +307,7 @@ func (p *planBuilder) planCall(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallZero(expr ast.Expr,
|
||||
function string,
|
||||
overload string,
|
||||
impl *functions.Overload) (Interpretable, error) {
|
||||
impl *functions.Overload) (InterpretableV2, error) {
|
||||
if impl == nil || impl.Function == nil {
|
||||
return nil, fmt.Errorf("no such overload: %s()", function)
|
||||
}
|
||||
@@ -324,7 +324,7 @@ func (p *planBuilder) planCallUnary(expr ast.Expr,
|
||||
function string,
|
||||
overload string,
|
||||
impl *functions.Overload,
|
||||
args []Interpretable) (Interpretable, error) {
|
||||
args []InterpretableV2) (InterpretableV2, error) {
|
||||
var fn functions.UnaryOp
|
||||
var trait int
|
||||
var nonStrict bool
|
||||
@@ -352,7 +352,7 @@ func (p *planBuilder) planCallBinary(expr ast.Expr,
|
||||
function string,
|
||||
overload string,
|
||||
impl *functions.Overload,
|
||||
args []Interpretable) (Interpretable, error) {
|
||||
args []InterpretableV2) (InterpretableV2, error) {
|
||||
var fn functions.BinaryOp
|
||||
var trait int
|
||||
var nonStrict bool
|
||||
@@ -381,7 +381,7 @@ func (p *planBuilder) planCallVarArgs(expr ast.Expr,
|
||||
function string,
|
||||
overload string,
|
||||
impl *functions.Overload,
|
||||
args []Interpretable) (Interpretable, error) {
|
||||
args []InterpretableV2) (InterpretableV2, error) {
|
||||
var fn functions.FunctionOp
|
||||
var trait int
|
||||
var nonStrict bool
|
||||
@@ -405,7 +405,7 @@ func (p *planBuilder) planCallVarArgs(expr ast.Expr,
|
||||
}
|
||||
|
||||
// planCallEqual generates an equals (==) Interpretable.
|
||||
func (p *planBuilder) planCallEqual(expr ast.Expr, args []Interpretable) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallEqual(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) {
|
||||
return &evalEq{
|
||||
id: expr.ID(),
|
||||
lhs: args[0],
|
||||
@@ -414,7 +414,7 @@ func (p *planBuilder) planCallEqual(expr ast.Expr, args []Interpretable) (Interp
|
||||
}
|
||||
|
||||
// planCallNotEqual generates a not equals (!=) Interpretable.
|
||||
func (p *planBuilder) planCallNotEqual(expr ast.Expr, args []Interpretable) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallNotEqual(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) {
|
||||
return &evalNe{
|
||||
id: expr.ID(),
|
||||
lhs: args[0],
|
||||
@@ -423,7 +423,7 @@ func (p *planBuilder) planCallNotEqual(expr ast.Expr, args []Interpretable) (Int
|
||||
}
|
||||
|
||||
// planCallLogicalAnd generates a logical and (&&) Interpretable.
|
||||
func (p *planBuilder) planCallLogicalAnd(expr ast.Expr, args []Interpretable) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallLogicalAnd(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) {
|
||||
return &evalAnd{
|
||||
id: expr.ID(),
|
||||
terms: args,
|
||||
@@ -431,7 +431,7 @@ func (p *planBuilder) planCallLogicalAnd(expr ast.Expr, args []Interpretable) (I
|
||||
}
|
||||
|
||||
// planCallLogicalOr generates a logical or (||) Interpretable.
|
||||
func (p *planBuilder) planCallLogicalOr(expr ast.Expr, args []Interpretable) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallLogicalOr(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) {
|
||||
return &evalOr{
|
||||
id: expr.ID(),
|
||||
terms: args,
|
||||
@@ -439,7 +439,7 @@ func (p *planBuilder) planCallLogicalOr(expr ast.Expr, args []Interpretable) (In
|
||||
}
|
||||
|
||||
// planCallConditional generates a conditional / ternary (c ? t : f) Interpretable.
|
||||
func (p *planBuilder) planCallConditional(expr ast.Expr, args []Interpretable) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallConditional(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) {
|
||||
cond := args[0]
|
||||
t := args[1]
|
||||
var tAttr Attribute
|
||||
@@ -467,7 +467,7 @@ func (p *planBuilder) planCallConditional(expr ast.Expr, args []Interpretable) (
|
||||
|
||||
// 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 *planBuilder) planCallIndex(expr ast.Expr, args []Interpretable, optional bool) (Interpretable, error) {
|
||||
func (p *planBuilder) planCallIndex(expr ast.Expr, args []InterpretableV2, optional bool) (InterpretableV2, error) {
|
||||
op := args[0]
|
||||
ind := args[1]
|
||||
opType := p.typeMap[op.ID()]
|
||||
@@ -502,7 +502,7 @@ func (p *planBuilder) planCallIndex(expr ast.Expr, args []Interpretable, optiona
|
||||
}
|
||||
|
||||
// planCreateList generates a list construction Interpretable.
|
||||
func (p *planBuilder) planCreateList(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planCreateList(expr ast.Expr) (InterpretableV2, error) {
|
||||
list := expr.AsList()
|
||||
optionalIndices := list.OptionalIndices()
|
||||
elements := list.Elements()
|
||||
@@ -513,7 +513,7 @@ func (p *planBuilder) planCreateList(expr ast.Expr) (Interpretable, error) {
|
||||
}
|
||||
optionals[index] = true
|
||||
}
|
||||
elems := make([]Interpretable, len(elements))
|
||||
elems := make([]InterpretableV2, len(elements))
|
||||
for i, elem := range elements {
|
||||
elemVal, err := p.plan(elem)
|
||||
if err != nil {
|
||||
@@ -531,12 +531,12 @@ func (p *planBuilder) planCreateList(expr ast.Expr) (Interpretable, error) {
|
||||
}
|
||||
|
||||
// planCreateStruct generates a map or object construction Interpretable.
|
||||
func (p *planBuilder) planCreateMap(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planCreateMap(expr ast.Expr) (InterpretableV2, error) {
|
||||
m := expr.AsMap()
|
||||
entries := m.Entries()
|
||||
optionals := make([]bool, len(entries))
|
||||
keys := make([]Interpretable, len(entries))
|
||||
vals := make([]Interpretable, len(entries))
|
||||
keys := make([]InterpretableV2, len(entries))
|
||||
vals := make([]InterpretableV2, len(entries))
|
||||
hasOptionals := false
|
||||
for i, e := range entries {
|
||||
entry := e.AsMapEntry()
|
||||
@@ -565,7 +565,7 @@ func (p *planBuilder) planCreateMap(expr ast.Expr) (Interpretable, error) {
|
||||
}
|
||||
|
||||
// planCreateObj generates an object construction Interpretable.
|
||||
func (p *planBuilder) planCreateStruct(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planCreateStruct(expr ast.Expr) (InterpretableV2, error) {
|
||||
obj := expr.AsStruct()
|
||||
typeName, defined := p.resolveTypeName(obj.TypeName())
|
||||
if !defined {
|
||||
@@ -574,7 +574,7 @@ func (p *planBuilder) planCreateStruct(expr ast.Expr) (Interpretable, error) {
|
||||
objFields := obj.Fields()
|
||||
optionals := make([]bool, len(objFields))
|
||||
fields := make([]string, len(objFields))
|
||||
vals := make([]Interpretable, len(objFields))
|
||||
vals := make([]InterpretableV2, len(objFields))
|
||||
hasOptionals := false
|
||||
for i, f := range objFields {
|
||||
field := f.AsStructField()
|
||||
@@ -599,7 +599,7 @@ func (p *planBuilder) planCreateStruct(expr ast.Expr) (Interpretable, error) {
|
||||
}
|
||||
|
||||
// planComprehension generates an Interpretable fold operation.
|
||||
func (p *planBuilder) planComprehension(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planComprehension(expr ast.Expr) (InterpretableV2, error) {
|
||||
fold := expr.AsComprehension()
|
||||
accu, err := p.plan(fold.AccuInit())
|
||||
if err != nil {
|
||||
@@ -639,7 +639,7 @@ func (p *planBuilder) planComprehension(expr ast.Expr) (Interpretable, error) {
|
||||
}
|
||||
|
||||
// planConst generates a constant valued Interpretable.
|
||||
func (p *planBuilder) planConst(expr ast.Expr) (Interpretable, error) {
|
||||
func (p *planBuilder) planConst(expr ast.Expr) (InterpretableV2, error) {
|
||||
return NewConstValue(expr.ID(), expr.AsLiteral()), nil
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ func (p *planBuilder) resolveFunction(expr ast.Expr) (ast.Expr, string, string)
|
||||
|
||||
// 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 *planBuilder) relativeAttr(id int64, eval Interpretable, opt bool) (InterpretableAttribute, error) {
|
||||
func (p *planBuilder) relativeAttr(id int64, eval InterpretableV2, opt bool) (InterpretableAttribute, error) {
|
||||
eAttr, ok := eval.(InterpretableAttribute)
|
||||
if !ok {
|
||||
eAttr = &evalAttr{
|
||||
|
||||
+36
-53
@@ -62,48 +62,6 @@ func CostObserver(opts ...costTrackPlanOption) PlannerOption {
|
||||
}
|
||||
}
|
||||
|
||||
// costTrackerConverter identifies an object which is convertible to a CostTracker instance.
|
||||
type costTrackerConverter interface {
|
||||
asCostTracker() *CostTracker
|
||||
}
|
||||
|
||||
// costTrackActivation hides state in the Activation in a manner not accessible to expressions.
|
||||
type costTrackActivation struct {
|
||||
vars Activation
|
||||
costTracker *CostTracker
|
||||
}
|
||||
|
||||
// ResolveName proxies variable lookups to the backing activation.
|
||||
func (cta costTrackActivation) ResolveName(name string) (any, bool) {
|
||||
return cta.vars.ResolveName(name)
|
||||
}
|
||||
|
||||
// Parent proxies parent lookups to the backing activation.
|
||||
func (cta costTrackActivation) Parent() Activation {
|
||||
return cta.vars
|
||||
}
|
||||
|
||||
// AsPartialActivation supports conversion to a partial activation in order to detect unknown attributes.
|
||||
func (cta costTrackActivation) AsPartialActivation() (PartialActivation, bool) {
|
||||
return AsPartialActivation(cta.vars)
|
||||
}
|
||||
|
||||
// asCostTracker implements the costTrackerConverter method.
|
||||
func (cta costTrackActivation) asCostTracker() *CostTracker {
|
||||
return cta.costTracker
|
||||
}
|
||||
|
||||
// asCostTracker walks the Activation hierarchy and returns the first cost tracker found, if present.
|
||||
func asCostTracker(vars Activation) (*CostTracker, bool) {
|
||||
if conv, ok := vars.(costTrackerConverter); ok {
|
||||
return conv.asCostTracker(), true
|
||||
}
|
||||
if vars.Parent() != nil {
|
||||
return asCostTracker(vars.Parent())
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// costTrackerFactory holds a factory for producing new CostTracker instances on each Eval call.
|
||||
type costTrackerFactory struct {
|
||||
factory func() (*CostTracker, error)
|
||||
@@ -111,27 +69,37 @@ type costTrackerFactory struct {
|
||||
|
||||
// InitState produces a CostTracker and bundles it into an Activation in a way which is not visible
|
||||
// to expression evaluation.
|
||||
func (ct *costTrackerFactory) InitState(vars Activation) (Activation, error) {
|
||||
func (ct *costTrackerFactory) InitState(frame *ExecutionFrame) (any, error) {
|
||||
tracker, err := ct.factory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return costTrackActivation{vars: vars, costTracker: tracker}, nil
|
||||
if frame.ctx == nil {
|
||||
frame.ctx = evalContextPool.Get().(*evalContext)
|
||||
}
|
||||
frame.ctx.costs = tracker
|
||||
return tracker, nil
|
||||
}
|
||||
|
||||
// GetState extracts the CostTracker from the Activation.
|
||||
func (ct *costTrackerFactory) GetState(vars Activation) any {
|
||||
if tracker, found := asCostTracker(vars); found {
|
||||
return tracker
|
||||
func (ct *costTrackerFactory) GetState(frame *ExecutionFrame) any {
|
||||
if frame == nil || frame.ctx == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return frame.ctx.costs
|
||||
}
|
||||
|
||||
// Observe computes the incremental cost of each step and records it into the CostTracker associated
|
||||
// with the evaluation.
|
||||
func (ct *costTrackerFactory) Observe(vars Activation, id int64, programStep any, val ref.Val) {
|
||||
tracker, found := asCostTracker(vars)
|
||||
if !found {
|
||||
frame := AsFrame(vars)
|
||||
state := ct.GetState(frame)
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
tracker, ok := state.(*CostTracker)
|
||||
if !ok {
|
||||
// The state is configured with CostTrackFactory so this shouldn't happen.
|
||||
return
|
||||
}
|
||||
switch t := programStep.(type) {
|
||||
@@ -265,6 +233,19 @@ type CostTracker struct {
|
||||
stack refValStack
|
||||
}
|
||||
|
||||
// Clone makes a shallow copy of the tracker.
|
||||
// The different clones can be used independently from
|
||||
// each other.
|
||||
func (c *CostTracker) Clone() (*CostTracker, error) {
|
||||
tracker := &CostTracker{
|
||||
Estimator: c.Estimator,
|
||||
overloadTrackers: c.overloadTrackers,
|
||||
Limit: c.Limit,
|
||||
presenceTestHasCost: c.presenceTestHasCost,
|
||||
}
|
||||
return tracker, nil
|
||||
}
|
||||
|
||||
// ActualCost returns the runtime cost
|
||||
func (c *CostTracker) ActualCost() uint64 {
|
||||
return c.cost
|
||||
@@ -292,7 +273,9 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re
|
||||
// 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:
|
||||
case overloads.StartsWithString, overloads.EndsWithString:
|
||||
cost += uint64(math.Ceil(float64(actualSize(args[1])) * common.StringTraversalCostFactor))
|
||||
case overloads.StringToBytes, overloads.BytesToString, overloads.ExtQuoteString, overloads.ExtFormatString:
|
||||
cost += uint64(math.Ceil(float64(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.
|
||||
@@ -317,7 +300,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re
|
||||
// In the worst case scenario, we would need to reallocate a new backing store and copy both operands over.
|
||||
cost += uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1])) * common.StringTraversalCostFactor))
|
||||
// O(nm) functions
|
||||
case overloads.MatchesString:
|
||||
case overloads.Matches, 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.
|
||||
@@ -397,7 +380,7 @@ func (s *refValStack) drop(ids ...int64) {
|
||||
// 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) {
|
||||
func (s *refValStack) dropArgs(args []InterpretableV2) ([]ref.Val, bool) {
|
||||
result := make([]ref.Val, len(args))
|
||||
argloop:
|
||||
for nIdx := len(args) - 1; nIdx >= 0; nIdx-- {
|
||||
|
||||
+1
-1
@@ -297,7 +297,7 @@ func (un *unparser) visitConstVal(val ref.Val) error {
|
||||
// represent the float using the minimum required digits
|
||||
d := strconv.FormatFloat(float64(val), 'g', -1, 64)
|
||||
un.str.WriteString(d)
|
||||
if !strings.Contains(d, ".") {
|
||||
if !strings.ContainsAny(d, ".eE") {
|
||||
un.str.WriteString(".0")
|
||||
}
|
||||
case types.Int:
|
||||
|
||||
Vendored
+1
-1
@@ -358,7 +358,7 @@ github.com/golang/protobuf/ptypes/timestamp
|
||||
# github.com/google/btree v1.1.3
|
||||
## explicit; go 1.18
|
||||
github.com/google/btree
|
||||
# github.com/google/cel-go v0.28.1
|
||||
# github.com/google/cel-go v0.29.0
|
||||
## explicit; go 1.23.0
|
||||
github.com/google/cel-go/cel
|
||||
github.com/google/cel-go/checker
|
||||
|
||||
Reference in New Issue
Block a user