🌱 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:
dependabot[bot]
2026-07-04 00:43:52 +00:00
committed by GitHub
co-authored by lnx01
parent 745931c353
commit 43344d0ffb
33 changed files with 2071 additions and 849 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
)
File diff suppressed because it is too large Load Diff
+44 -76
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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