diff --git a/LICENSES/vendor/github.com/google/cel-go/LICENSE b/LICENSES/vendor/cel.dev/cel-go/LICENSE similarity index 99% rename from LICENSES/vendor/github.com/google/cel-go/LICENSE rename to LICENSES/vendor/cel.dev/cel-go/LICENSE index 3b21976bd0b..fbb986632fc 100644 --- a/LICENSES/vendor/github.com/google/cel-go/LICENSE +++ b/LICENSES/vendor/cel.dev/cel-go/LICENSE @@ -1,4 +1,4 @@ -= vendor/github.com/google/cel-go licensed under: = += vendor/cel.dev/cel-go licensed under: = Apache License @@ -234,4 +234,4 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -= vendor/github.com/google/cel-go/LICENSE 9e40c7725e55fa8f61a69abf908e2c6f += vendor/cel.dev/cel-go/LICENSE 9e40c7725e55fa8f61a69abf908e2c6f diff --git a/go.mod b/go.mod index cf24beba3c4..754c2a9346e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ godebug default=go1.27 require ( bitbucket.org/bertimus9/systemstat v0.5.0 + cel.dev/cel-go v0.33.0 github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hnslib v0.1.3 @@ -32,7 +33,6 @@ require ( github.com/go-openapi/jsonreference v1.0.2 github.com/godbus/dbus/v5 v5.2.4 github.com/google/cadvisor/lib v0.60.6 - github.com/google/cel-go v0.29.2 github.com/google/gnostic-models v0.7.1 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index a220dc474da..dd9d4e49dea 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ bitbucket.org/bertimus9/systemstat v0.5.0 h1:n0aLnh2Jo4nBUBym9cE5PJDG8GT6g+4VuS2 bitbucket.org/bertimus9/systemstat v0.5.0/go.mod h1:EkUWPp8lKFPMXP8vnbpT5JDI0W/sTiLZAvN8ONWErHY= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -152,8 +154,7 @@ 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/cadvisor/lib v0.60.6 h1:VX5+magaxafSlozQiLqo2zbHByciW5IBafASCmySZUw= github.com/google/cadvisor/lib v0.60.6/go.mod h1:htHKT0OSYO6zaik+iLOo3Wj1mf/5l8uV5TJaqLz13oM= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/go.work.sum b/go.work.sum index e758c204892..1438e1a396f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -15,6 +15,7 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/hack/unwanted-dependencies.json b/hack/unwanted-dependencies.json index 494ee02c374..4934b272c2f 100644 --- a/hack/unwanted-dependencies.json +++ b/hack/unwanted-dependencies.json @@ -166,8 +166,8 @@ "google.golang.org/grpc" ], "golang.org/x/exp": [ + "cel.dev/cel-go", "github.com/antlr4-go/antlr/v4", - "github.com/google/cel-go", "github.com/grpc-ecosystem/go-grpc-middleware/v2" ], "gopkg.in/yaml.v3": [ diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index 2b1211b8877..61815464eec 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/staging/src/k8s.io/apiextensions-apiserver/go.mod b/staging/src/k8s.io/apiextensions-apiserver/go.mod index 4e0bfed99f1..e0deaee5e85 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/go.mod +++ b/staging/src/k8s.io/apiextensions-apiserver/go.mod @@ -7,8 +7,8 @@ go 1.27.0 godebug default=go1.27 require ( + cel.dev/cel-go v0.33.0 github.com/emicklei/go-restful/v3 v3.14.0 - github.com/google/cel-go v0.29.2 github.com/google/gnostic-models v0.7.1 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 diff --git a/staging/src/k8s.io/apiextensions-apiserver/go.sum b/staging/src/k8s.io/apiextensions-apiserver/go.sum index a41e553d7f4..e37efaaa14d 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/go.sum +++ b/staging/src/k8s.io/apiextensions-apiserver/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -83,8 +85,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index 5b7f748f4ad..2e4ec307990 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -27,7 +27,7 @@ import ( "unicode" "unicode/utf8" - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" apiextensionsv1 "k8s.io/apiextensions-api/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-api/pkg/apis/apiextensions/v1beta1" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go index 951f187d769..2caa21acd73 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go @@ -24,7 +24,7 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" apiextensionsv1beta1 "k8s.io/apiextensions-api/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go index 6bfa41349db..655f2140067 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go @@ -22,9 +22,9 @@ import ( "strings" "time" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/checker" + "cel.dev/cel-go/common/types" apiextensions "k8s.io/apiextensions-api/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go index f11596b4cd8..e4d47bf6732 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go @@ -22,9 +22,9 @@ import ( "strings" "testing" - celgo "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + celgo "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" apiextensions "k8s.io/apiextensions-api/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas_test.go index c6c99133cd4..6be6ede2651 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas_test.go @@ -20,7 +20,7 @@ import ( "reflect" "testing" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/types" "google.golang.org/protobuf/proto" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/types_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/types_test.go index deaba8208c4..2ed6be9115e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/types_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/types_test.go @@ -19,8 +19,8 @@ package model import ( "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" apiservercel "k8s.io/apiserver/pkg/cel" ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go index 0342cec58b4..08d36327b7c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go @@ -26,10 +26,10 @@ import ( "strings" "time" - celgo "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" + celgo "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" apiextensions "k8s.io/apiextensions-api/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go index 8b879eaf206..6b591f01f8d 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go @@ -17,7 +17,7 @@ limitations under the License. package cel import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go index 729ab4dd256..7fe83ae7a10 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go @@ -20,9 +20,9 @@ import ( "reflect" "testing" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" ) diff --git a/staging/src/k8s.io/apiserver/go.mod b/staging/src/k8s.io/apiserver/go.mod index 5903ee76a4a..03918a1349b 100644 --- a/staging/src/k8s.io/apiserver/go.mod +++ b/staging/src/k8s.io/apiserver/go.mod @@ -7,6 +7,7 @@ go 1.27.0 godebug default=go1.27 require ( + cel.dev/cel-go v0.33.0 github.com/blang/semver/v4 v4.0.0 github.com/coreos/go-oidc/v3 v3.21.0 github.com/coreos/go-systemd/v22 v22.7.0 @@ -15,7 +16,6 @@ require ( github.com/go-jose/go-jose/v4 v4.1.5 github.com/go-logr/logr v1.4.4 github.com/go-openapi/jsonreference v1.0.2 - github.com/google/cel-go v0.29.2 github.com/google/gnostic-models v0.7.1 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 diff --git a/staging/src/k8s.io/apiserver/go.sum b/staging/src/k8s.io/apiserver/go.sum index ccce07f6c34..4b9590d2fe9 100644 --- a/staging/src/k8s.io/apiserver/go.sum +++ b/staging/src/k8s.io/apiserver/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -84,8 +86,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/apiserver/pkg/admission/conversion.go b/staging/src/k8s.io/apiserver/pkg/admission/conversion.go index a5885b5fbd1..51da7aa86d4 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/conversion.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/conversion.go @@ -19,8 +19,8 @@ package admission import ( "reflect" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/conversion_test.go b/staging/src/k8s.io/apiserver/pkg/admission/conversion_test.go index 716a50ab6fb..3283a05938e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/conversion_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/conversion_test.go @@ -21,9 +21,9 @@ import ( "reflect" "testing" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go index 45299eeaa9c..17415dab756 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go @@ -22,7 +22,7 @@ import ( "math" "time" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/interpreter" admissionv1 "k8s.io/api/admission/v1" v1 "k8s.io/api/core/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go index fb5dd825227..bddfe4d5c61 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go @@ -19,7 +19,7 @@ package cel import ( "fmt" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "k8s.io/apimachinery/pkg/util/version" celconfig "k8s.io/apiserver/pkg/apis/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go index 5caf82858a4..a67b16217fc 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/cel/environment" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go index d7456d090b3..6a4f4e429ef 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go @@ -20,10 +20,10 @@ import ( "context" "math" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" v1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go index 45859f5c274..02bcdc7f785 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/admission" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go index 12a009d8bcb..149e37b7620 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go @@ -24,8 +24,8 @@ import ( "strings" "testing" - celgo "github.com/google/cel-go/cel" - celtypes "github.com/google/cel-go/common/types" + celgo "cel.dev/cel-go/cel" + celtypes "cel.dev/cel-go/common/types" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go index a293eb38981..2ba8aec66d4 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go @@ -20,8 +20,8 @@ import ( "context" "time" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types/ref" v1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go index 439473d839c..fc05bf5f869 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go @@ -17,16 +17,16 @@ limitations under the License. package patch import ( + celgo "cel.dev/cel-go/cel" "context" gojson "encoding/json" "errors" "fmt" - celgo "github.com/google/cel-go/cel" "reflect" "strconv" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/traits" "google.golang.org/protobuf/types/known/structpb" jsonpatch "gopkg.in/evanphx/json-patch.v4" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go index b63b57ccec9..b58ae8e32c3 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go @@ -22,8 +22,8 @@ import ( "fmt" "strings" - celgo "github.com/google/cel-go/cel" - celtypes "github.com/google/cel-go/common/types" + celgo "cel.dev/cel-go/cel" + celtypes "cel.dev/cel-go/common/types" "sigs.k8s.io/structured-merge-diff/v7/fieldpath" "sigs.k8s.io/structured-merge-diff/v7/schema" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go index 77d8d3efebc..3b85a4da751 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go @@ -20,7 +20,7 @@ import ( "context" "io" - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" v1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go index 2f1ab4499a7..bccce88ebf0 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go @@ -19,7 +19,7 @@ package validating import ( "context" - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go index 8b7d973617d..528457c3d86 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go @@ -17,7 +17,7 @@ limitations under the License. package validating import ( - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" "k8s.io/apiserver/pkg/admission/plugin/cel" ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go index c8b258f1635..365369c50de 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go @@ -23,7 +23,7 @@ import ( "strings" "time" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" v1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/api/meta" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go index 1bff1167342..201be59f700 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go @@ -22,7 +22,7 @@ import ( "fmt" "strings" - celtypes "github.com/google/cel-go/common/types" + celtypes "cel.dev/cel-go/common/types" v1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go index 477b56feafd..650a9eaff1e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - celtypes "github.com/google/cel-go/common/types" + celtypes "cel.dev/cel-go/common/types" "github.com/stretchr/testify/require" admissionv1 "k8s.io/api/admission/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go index c8bde3d7d51..42b9806dbd1 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go @@ -22,8 +22,8 @@ import ( "fmt" "time" - "github.com/google/cel-go/cel" - celtypes "github.com/google/cel-go/common/types" + "cel.dev/cel-go/cel" + celtypes "cel.dev/cel-go/common/types" v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go index 153ebb3b5af..4ba97a0ac25 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go @@ -26,7 +26,7 @@ import ( v1 "k8s.io/api/admissionregistration/v1" - celtypes "github.com/google/cel-go/common/types" + celtypes "cel.dev/cel-go/common/types" "github.com/stretchr/testify/require" admissionv1 "k8s.io/api/admission/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/testing/celtest/evaluator.go b/staging/src/k8s.io/apiserver/pkg/admission/testing/celtest/evaluator.go index 17f18010e8d..97db18ff37d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/testing/celtest/evaluator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/testing/celtest/evaluator.go @@ -39,7 +39,7 @@ import ( "fmt" "strings" - celtypes "github.com/google/cel-go/common/types" + celtypes "cel.dev/cel-go/common/types" "google.golang.org/protobuf/types/known/structpb" admissionv1 "k8s.io/api/admission/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go index 000b16d77b2..2dbe2ebd683 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go @@ -25,8 +25,8 @@ import ( "strings" "time" - celgo "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/operators" + celgo "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/operators" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" v1 "k8s.io/api/authorization/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go index 6f25353cc98..380a34efcba 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go @@ -19,7 +19,7 @@ package cel import ( "fmt" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "k8s.io/apimachinery/pkg/util/version" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go index be7e634f367..6c16e7786c8 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go @@ -20,9 +20,9 @@ package cel import ( "context" - celgo "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + celgo "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // ExpressionAccessor is an interface that provides access to a CEL expression. diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go index 3b6041aeb5b..d10d0882480 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go @@ -20,8 +20,8 @@ import ( "context" "fmt" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/interpreter" ) var _ ClaimsMapper = &mapper{} diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/conditions_test.go b/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/conditions_test.go index 0a34f04d7cf..fc63b3bc754 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/conditions_test.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/conditions_test.go @@ -28,7 +28,7 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go index b703d87b3c7..4a999b22ee2 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go @@ -19,10 +19,10 @@ package cel import ( "fmt" - "github.com/google/cel-go/cel" - celast "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + celast "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types/ref" authorizationv1 "k8s.io/api/authorization/v1" "k8s.io/apimachinery/pkg/util/version" diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/authorization/cel/interface.go index 82166830c87..88337f7c9ee 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/cel/interface.go @@ -17,7 +17,7 @@ limitations under the License. package cel import ( - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" ) type ExpressionAccessor interface { diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/cel/matcher.go b/staging/src/k8s.io/apiserver/pkg/authorization/cel/matcher.go index 372404d5c28..3b6af12b4ae 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/cel/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/cel/matcher.go @@ -21,7 +21,7 @@ import ( "fmt" "time" - celgo "github.com/google/cel-go/cel" + celgo "cel.dev/cel-go/cel" authorizationv1 "k8s.io/api/authorization/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/cidr.go b/staging/src/k8s.io/apiserver/pkg/cel/cidr.go index 8e97f63cd74..f1102fe4e0e 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/cidr.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/cidr.go @@ -22,9 +22,9 @@ import ( "net/netip" "reflect" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // CIDR provides a CEL representation of an network address. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go b/staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go index 909284166ab..0dfb6f79ee4 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go @@ -19,8 +19,8 @@ package common import ( "time" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/kube-openapi/pkg/validation/spec" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go index 7d972193305..4ff81251f20 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go @@ -17,9 +17,9 @@ limitations under the License. package common import ( - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // TypeResolver resolves a type by a given name. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go index 932b469b12b..d929037ee41 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go @@ -21,9 +21,9 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/cel/environment" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/values_test.go b/staging/src/k8s.io/apiserver/pkg/cel/common/values_test.go index d108f87434e..9a0808aaeb5 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/values_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/values_test.go @@ -21,9 +21,9 @@ import ( "testing" "time" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect.go b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect.go index c2d51ffda62..2d002c66b9b 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect.go @@ -30,9 +30,9 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apiserver/pkg/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // TypedToVal wraps "typed" Go value as CEL ref.Val types using reflection. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect_test.go b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect_test.go index 1cbc6ce5937..7a1136d4c02 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesreflect_test.go @@ -38,8 +38,8 @@ import ( "reflect" "testing" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) type keyTestEntry struct { diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped.go b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped.go index ba629a606bb..155aa9522fc 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped.go @@ -21,9 +21,9 @@ import ( "fmt" "reflect" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "sigs.k8s.io/structured-merge-diff/v7/value" apiequality "k8s.io/apimachinery/pkg/api/equality" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped_test.go b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped_test.go index 4f0740ca972..3adbd7ac084 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesschemalesstyped_test.go @@ -23,9 +23,9 @@ import ( "testing" "time" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesunstructured.go b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesunstructured.go index 377f885b700..418028273d2 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/common/valuesunstructured.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/valuesunstructured.go @@ -22,9 +22,9 @@ import ( "sync" "time" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "k8s.io/kube-openapi/pkg/validation/strfmt" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go b/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go index a0644454aa8..96ff2a5fc49 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/version" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/environment/environment.go b/staging/src/k8s.io/apiserver/pkg/cel/environment/environment.go index 07b9e8f5466..a792e90c6df 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/environment/environment.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/environment/environment.go @@ -20,7 +20,7 @@ import ( "fmt" "math" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "k8s.io/apimachinery/pkg/util/version" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/environment/environment_test.go b/staging/src/k8s.io/apiserver/pkg/cel/environment/environment_test.go index b6702faccf3..5d5a4b5c6b8 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/environment/environment_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/environment/environment_test.go @@ -21,7 +21,7 @@ import ( "fmt" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/authorization/authorizer" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/errors.go b/staging/src/k8s.io/apiserver/pkg/cel/errors.go index d7b052fc9e7..df8d902d1e7 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/errors.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/errors.go @@ -19,7 +19,7 @@ package cel import ( "fmt" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" ) // ErrInternal the basic error that occurs when the expression fails to evaluate diff --git a/staging/src/k8s.io/apiserver/pkg/cel/errors_test.go b/staging/src/k8s.io/apiserver/pkg/cel/errors_test.go index 2822aaab714..595c4ee04f2 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/errors_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/errors_test.go @@ -20,7 +20,7 @@ import ( "errors" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" ) func TestOutOfBudgetError(t *testing.T) { diff --git a/staging/src/k8s.io/apiserver/pkg/cel/format.go b/staging/src/k8s.io/apiserver/pkg/cel/format.go index 31216806f78..0be731acf3b 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/format.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/format.go @@ -20,10 +20,10 @@ import ( "fmt" "reflect" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) var ( diff --git a/staging/src/k8s.io/apiserver/pkg/cel/ip.go b/staging/src/k8s.io/apiserver/pkg/cel/ip.go index f91c6cb7a86..7409837b89c 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/ip.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/ip.go @@ -22,9 +22,9 @@ import ( "net/netip" "reflect" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // IP provides a CEL representation of an IP address. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy.go b/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy.go index 16183050d9b..c4f2dd66bb4 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy.go @@ -20,9 +20,9 @@ import ( "fmt" "reflect" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "k8s.io/apiserver/pkg/cel" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy_test.go b/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy_test.go index 01fc725e7e9..b7dfb5454cb 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/lazy/lazy_test.go @@ -20,10 +20,10 @@ import ( "fmt" "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" "k8s.io/apimachinery/pkg/util/version" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go b/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go index beefaf64543..b6e3aa84a47 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go @@ -25,9 +25,9 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go index 9106e46d7db..f5ffeb079a2 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go @@ -22,9 +22,9 @@ import ( "math" "net/netip" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" apiservercel "k8s.io/apiserver/pkg/cel" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cidr_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cidr_test.go index 73ac6581798..261fd39472f 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cidr_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cidr_test.go @@ -21,9 +21,9 @@ import ( "regexp" "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/format.go b/staging/src/k8s.io/apiserver/pkg/cel/library/format.go index e8b2d07b557..1726e7ec021 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/format.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/format.go @@ -20,10 +20,10 @@ import ( "fmt" "net/url" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/util/validation" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go index c6bc792a130..26bb978c565 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go @@ -20,8 +20,8 @@ import ( "fmt" "testing" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/library" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go b/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go index fa5e9f001dd..4bfe9b9fb1f 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go @@ -20,9 +20,9 @@ import ( "fmt" "net/netip" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" apiservercel "k8s.io/apiserver/pkg/cel" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/ip_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/ip_test.go index 536636d55ab..c29f1ac4dd2 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/ip_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/ip_test.go @@ -21,9 +21,9 @@ import ( "regexp" "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go index bdcb6d852b0..6dd2b63387d 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go @@ -17,9 +17,9 @@ limitations under the License. package library import ( - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "strings" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go index dc436973e5a..faec83e4d6e 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go @@ -17,7 +17,7 @@ limitations under the License. package library import ( - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" ) // Library represents a CEL library used by kubernetes. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go index 014c46f1589..2b56d73e447 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go @@ -20,9 +20,9 @@ import ( "strings" "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/types" "k8s.io/apimachinery/pkg/util/sets" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go b/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go index 43933249bcf..51494cdf9e7 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go @@ -20,11 +20,11 @@ import ( "fmt" "math" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter/functions" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/interpreter/functions" ) // Lists provides a CEL function library extension of list utility functions. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go b/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go index 011c383c4f3..21361ffb3cf 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go @@ -19,9 +19,9 @@ package library import ( "errors" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "k8s.io/apimachinery/pkg/api/resource" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go b/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go index 2cf8b00375c..14f53d6537c 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go @@ -19,10 +19,10 @@ package library import ( "regexp" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" ) // Regex provides a CEL function library extension of regex utility functions. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/semver_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/semver_test.go index 77e2b809c5c..38277de83df 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/semver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/semver_test.go @@ -20,10 +20,10 @@ import ( "regexp" "testing" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "github.com/blang/semver/v4" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/semverlib.go b/staging/src/k8s.io/apiserver/pkg/cel/library/semverlib.go index e7193c2a707..35a977c72db 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/semverlib.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/semverlib.go @@ -21,10 +21,10 @@ import ( "math" "strings" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "github.com/blang/semver/v4" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" apiservercel "k8s.io/apiserver/pkg/cel" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/test.go index 282d9396208..c9003df2c42 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/test.go @@ -19,9 +19,9 @@ package library import ( "math" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // Test provides a test() function that returns true. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go b/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go index 4b7ffb95a4e..912101b0291 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go @@ -19,9 +19,9 @@ package library import ( "net/url" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" apiservercel "k8s.io/apiserver/pkg/cel" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go index 80167b0b63a..782b54988f0 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go @@ -22,9 +22,9 @@ import ( "reflect" "strings" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go index 77df052ec17..088537734fe 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go @@ -21,8 +21,8 @@ import ( "strings" "testing" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) func TestOptional(t *testing.T) { diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go index 1f6d63fa22e..249d31dc6f1 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go @@ -17,10 +17,10 @@ limitations under the License. package mutation import ( + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "fmt" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" "reflect" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go index a5bfac26206..38e1de08f4c 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go @@ -17,7 +17,7 @@ limitations under the License. package openapi import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/common" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go index 1799c2b0da7..ba53ef20fb9 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go @@ -19,9 +19,9 @@ package openapi import ( "testing" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/interpreter" "k8s.io/apimachinery/pkg/util/version" apiservercel "k8s.io/apiserver/pkg/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas_test.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas_test.go index 80499c54b57..b8c9a363d30 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas_test.go @@ -20,7 +20,7 @@ import ( "reflect" "testing" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/types" "google.golang.org/protobuf/proto" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/values_test.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/values_test.go index 299e60055a7..4ff4271a76d 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/values_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/values_test.go @@ -20,9 +20,9 @@ import ( "reflect" "testing" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "k8s.io/kube-openapi/pkg/validation/spec" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/quantity.go b/staging/src/k8s.io/apiserver/pkg/cel/quantity.go index ce823964369..895eb69e5a3 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/quantity.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/quantity.go @@ -20,10 +20,10 @@ import ( "fmt" "reflect" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "k8s.io/apimachinery/pkg/api/resource" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/semver.go b/staging/src/k8s.io/apiserver/pkg/cel/semver.go index c53b9c306a0..6d2cad29659 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/semver.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/semver.go @@ -20,10 +20,10 @@ import ( "fmt" "reflect" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" "github.com/blang/semver/v4" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" ) var ( diff --git a/staging/src/k8s.io/apiserver/pkg/cel/types.go b/staging/src/k8s.io/apiserver/pkg/cel/types.go index 84bfd7e658b..e70a3d4d96c 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/types.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/types.go @@ -21,10 +21,10 @@ import ( "math" "time" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "k8s.io/apimachinery/pkg/api/resource" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/types_test.go b/staging/src/k8s.io/apiserver/pkg/cel/types_test.go index f7056fa0d92..66c7ccb11b8 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/types_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/types_test.go @@ -19,7 +19,7 @@ package cel import ( "testing" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/types" "github.com/stretchr/testify/require" ) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/url.go b/staging/src/k8s.io/apiserver/pkg/cel/url.go index 6800205c9a9..de6f58b25ab 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/url.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/url.go @@ -21,10 +21,10 @@ import ( "net/url" "reflect" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // URL provides a CEL representation of a URL. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/value.go b/staging/src/k8s.io/apiserver/pkg/cel/value.go index 01c7f20acc9..66cbeb5e1c2 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/value.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/value.go @@ -22,9 +22,9 @@ import ( "sync" "time" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // EncodeStyle is a hint for string encoding of parsed values. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/value_test.go b/staging/src/k8s.io/apiserver/pkg/cel/value_test.go index 84d83bcee80..d12c4039762 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/value_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/value_test.go @@ -22,9 +22,9 @@ import ( "testing" "time" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) func TestConvertToType(t *testing.T) { diff --git a/staging/src/k8s.io/apiserver/pkg/sharding/parser.go b/staging/src/k8s.io/apiserver/pkg/sharding/parser.go index fe08eac0c9b..84fdbe74c4f 100644 --- a/staging/src/k8s.io/apiserver/pkg/sharding/parser.go +++ b/staging/src/k8s.io/apiserver/pkg/sharding/parser.go @@ -20,11 +20,11 @@ import ( "fmt" "strings" - celparser "github.com/google/cel-go/parser" + celparser "cel.dev/cel-go/parser" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" apisharding "k8s.io/apimachinery/pkg/sharding" ) diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go index 2ca86b9f393..a9f17667b48 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go @@ -44,12 +44,12 @@ import ( "sync/atomic" "time" + celgo "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" "github.com/coreos/go-oidc/v3/oidc" jose "github.com/go-jose/go-jose/v4" - celgo "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" "golang.org/x/time/rate" "k8s.io/apimachinery/pkg/util/net" diff --git a/staging/src/k8s.io/cloud-provider/go.mod b/staging/src/k8s.io/cloud-provider/go.mod index 830d5e6d55e..a90fd9288e4 100644 --- a/staging/src/k8s.io/cloud-provider/go.mod +++ b/staging/src/k8s.io/cloud-provider/go.mod @@ -24,6 +24,7 @@ require ( ) require ( + cel.dev/cel-go v0.33.0 // indirect cel.dev/expr v0.25.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect @@ -48,7 +49,6 @@ require ( github.com/go-openapi/swag/pools v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.29.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect diff --git a/staging/src/k8s.io/cloud-provider/go.sum b/staging/src/k8s.io/cloud-provider/go.sum index 4c764d274c3..58977425c2d 100644 --- a/staging/src/k8s.io/cloud-provider/go.sum +++ b/staging/src/k8s.io/cloud-provider/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -78,8 +80,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/controller-manager/go.mod b/staging/src/k8s.io/controller-manager/go.mod index 25472c79c0e..eef38ba92cf 100644 --- a/staging/src/k8s.io/controller-manager/go.mod +++ b/staging/src/k8s.io/controller-manager/go.mod @@ -20,6 +20,7 @@ require ( ) require ( + cel.dev/cel-go v0.33.0 // indirect cel.dev/expr v0.25.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -42,7 +43,6 @@ require ( github.com/go-openapi/swag/pools v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.29.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect diff --git a/staging/src/k8s.io/controller-manager/go.sum b/staging/src/k8s.io/controller-manager/go.sum index 32f2895dc61..dc4c176897d 100644 --- a/staging/src/k8s.io/controller-manager/go.sum +++ b/staging/src/k8s.io/controller-manager/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -75,8 +77,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/dynamic-resource-allocation/cel/compile.go b/staging/src/k8s.io/dynamic-resource-allocation/cel/compile.go index a1b207b1cac..2acf0dc63d0 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/cel/compile.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/cel/compile.go @@ -26,12 +26,12 @@ import ( "strings" "sync" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/ext" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/checker" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/ext" "k8s.io/utils/ptr" diff --git a/staging/src/k8s.io/dynamic-resource-allocation/go.mod b/staging/src/k8s.io/dynamic-resource-allocation/go.mod index b811821357f..bd67371a07c 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/go.mod +++ b/staging/src/k8s.io/dynamic-resource-allocation/go.mod @@ -7,9 +7,9 @@ go 1.27.0 godebug default=go1.27 require ( + cel.dev/cel-go v0.33.0 github.com/blang/semver/v4 v4.0.0 github.com/go-logr/logr v1.4.4 - github.com/google/cel-go v0.29.2 github.com/google/go-cmp v0.7.0 github.com/onsi/gomega v1.44.0 github.com/stretchr/testify v1.12.1 diff --git a/staging/src/k8s.io/dynamic-resource-allocation/go.sum b/staging/src/k8s.io/dynamic-resource-allocation/go.sum index f75d2dc9b9b..972bfc451c3 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/go.sum +++ b/staging/src/k8s.io/dynamic-resource-allocation/go.sum @@ -1,3 +1,5 @@ +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -59,8 +61,6 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/kube-aggregator/go.mod b/staging/src/k8s.io/kube-aggregator/go.mod index c478f2f9096..d431b438927 100644 --- a/staging/src/k8s.io/kube-aggregator/go.mod +++ b/staging/src/k8s.io/kube-aggregator/go.mod @@ -31,6 +31,7 @@ require ( ) require ( + cel.dev/cel-go v0.33.0 // indirect cel.dev/expr v0.25.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -52,7 +53,6 @@ require ( github.com/go-openapi/swag/pools v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.29.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect diff --git a/staging/src/k8s.io/kube-aggregator/go.sum b/staging/src/k8s.io/kube-aggregator/go.sum index 4b10ac00222..bbaa05c3d5f 100644 --- a/staging/src/k8s.io/kube-aggregator/go.sum +++ b/staging/src/k8s.io/kube-aggregator/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -77,8 +79,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/kube-controller-manager/go.sum b/staging/src/k8s.io/kube-controller-manager/go.sum index 0680f9d7504..ce78df764c0 100644 --- a/staging/src/k8s.io/kube-controller-manager/go.sum +++ b/staging/src/k8s.io/kube-controller-manager/go.sum @@ -1,3 +1,4 @@ +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -25,7 +26,6 @@ github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2n github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc= github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/staging/src/k8s.io/kube-scheduler/go.sum b/staging/src/k8s.io/kube-scheduler/go.sum index f7224f6ad9b..a4d9faf493a 100644 --- a/staging/src/k8s.io/kube-scheduler/go.sum +++ b/staging/src/k8s.io/kube-scheduler/go.sum @@ -1,3 +1,4 @@ +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= @@ -37,7 +38,6 @@ github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2 github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/pod-security-admission/go.mod b/staging/src/k8s.io/pod-security-admission/go.mod index aca367fdfe0..e2504a682ca 100644 --- a/staging/src/k8s.io/pod-security-admission/go.mod +++ b/staging/src/k8s.io/pod-security-admission/go.mod @@ -23,6 +23,7 @@ require ( ) require ( + cel.dev/cel-go v0.33.0 // indirect cel.dev/expr v0.25.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -45,7 +46,6 @@ require ( github.com/go-openapi/swag/pools v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.29.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect diff --git a/staging/src/k8s.io/pod-security-admission/go.sum b/staging/src/k8s.io/pod-security-admission/go.sum index 32f2895dc61..dc4c176897d 100644 --- a/staging/src/k8s.io/pod-security-admission/go.sum +++ b/staging/src/k8s.io/pod-security-admission/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -75,8 +77,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/staging/src/k8s.io/sample-apiserver/go.mod b/staging/src/k8s.io/sample-apiserver/go.mod index 4f6d3c8b7b5..eb3c0fdb0b5 100644 --- a/staging/src/k8s.io/sample-apiserver/go.mod +++ b/staging/src/k8s.io/sample-apiserver/go.mod @@ -21,6 +21,7 @@ require ( ) require ( + cel.dev/cel-go v0.33.0 // indirect cel.dev/expr v0.25.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -43,7 +44,6 @@ require ( github.com/go-openapi/swag/pools v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.29.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/staging/src/k8s.io/sample-apiserver/go.sum b/staging/src/k8s.io/sample-apiserver/go.sum index c2ea66ea9fb..4d5468b1185 100644 --- a/staging/src/k8s.io/sample-apiserver/go.sum +++ b/staging/src/k8s.io/sample-apiserver/go.sum @@ -1,5 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/cel-go v0.33.0 h1:dVJNaNetMyzFG8iz7ZxOJDVvRHxpzwA4rrxxkTcsWQ0= +cel.dev/cel-go v0.33.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= @@ -76,8 +78,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm 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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/test/integration/apiserver/cel/typeresolution_test.go b/test/integration/apiserver/cel/typeresolution_test.go index ecbec133bcf..6c054d588b1 100644 --- a/test/integration/apiserver/cel/typeresolution_test.go +++ b/test/integration/apiserver/cel/typeresolution_test.go @@ -23,8 +23,8 @@ import ( "testing" "time" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/interpreter" "k8s.io/apiserver/pkg/cel/environment" diff --git a/test/integration/apiserver/conditionalauthorization/conditionalauthorization_test.go b/test/integration/apiserver/conditionalauthorization/conditionalauthorization_test.go index 2469558ff37..b4e22a5e819 100644 --- a/test/integration/apiserver/conditionalauthorization/conditionalauthorization_test.go +++ b/test/integration/apiserver/conditionalauthorization/conditionalauthorization_test.go @@ -30,7 +30,7 @@ import ( "sync/atomic" "testing" - "github.com/google/cel-go/cel" + "cel.dev/cel-go/cel" "github.com/google/go-cmp/cmp" admissionv1 "k8s.io/api/admission/v1" diff --git a/vendor/github.com/google/cel-go/LICENSE b/vendor/cel.dev/cel-go/LICENSE similarity index 100% rename from vendor/github.com/google/cel-go/LICENSE rename to vendor/cel.dev/cel-go/LICENSE diff --git a/vendor/github.com/google/cel-go/cel/BUILD.bazel b/vendor/cel.dev/cel-go/cel/BUILD.bazel similarity index 91% rename from vendor/github.com/google/cel-go/cel/BUILD.bazel rename to vendor/cel.dev/cel-go/cel/BUILD.bazel index 46cb26d625b..021cd370d90 100644 --- a/vendor/github.com/google/cel-go/cel/BUILD.bazel +++ b/vendor/cel.dev/cel-go/cel/BUILD.bazel @@ -23,14 +23,16 @@ go_library( "validator.go", ], embedsrcs = ["templates/authoring.tmpl"], - importpath = "github.com/google/cel-go/cel", + importpath = "cel.dev/cel-go/cel", visibility = ["//visibility:public"], deps = [ + "//cel/async:go_default_library", "//checker:go_default_library", "//checker/decls:go_default_library", "//common:go_default_library", "//common/ast:go_default_library", "//common/containers:go_default_library", + "//common/cost:go_default_library", "//common/decls:go_default_library", "//common/env:go_default_library", "//common/functions:go_default_library", @@ -63,13 +65,18 @@ go_test( srcs = [ "cel_example_test.go", "cel_test.go", + "cost_test.go", + "cost_k8s_test.go", "decls_test.go", + "env_ext_test.go", "env_test.go", "fieldpaths_test.go", "folding_test.go", "inlining_test.go", "io_test.go", + "memory_test.go", "optimizer_test.go", + "program_async_test.go", "prompt_test.go", "validator_test.go", ], @@ -84,6 +91,8 @@ go_test( "//cel/testdata:test_fds_with_source_info", ], deps = [ + "//cel/async:go_default_library", + "//common/cost:go_default_library", "//common/operators:go_default_library", "//common/overloads:go_default_library", "//common/types:go_default_library", diff --git a/vendor/cel.dev/cel-go/cel/async/BUILD.bazel b/vendor/cel.dev/cel-go/cel/async/BUILD.bazel new file mode 100644 index 00000000000..e77dfaec506 --- /dev/null +++ b/vendor/cel.dev/cel-go/cel/async/BUILD.bazel @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + licenses = ["notice"], # Apache 2.0 +) + +go_library( + name = "go_default_library", + srcs = [ + "async.go", + ], + importpath = "cel.dev/cel-go/cel/async", + visibility = ["//visibility:public"], + deps = [ + "//common/decls:go_default_library", + "//common/functions:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + "//interpreter:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "async_test.go", + ], + deps = [ + ":go_default_library", + "//common/decls:go_default_library", + "//common/functions:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + ], +) diff --git a/vendor/cel.dev/cel-go/cel/async/async.go b/vendor/cel.dev/cel-go/cel/async/async.go new file mode 100644 index 00000000000..b8621a784d7 --- /dev/null +++ b/vendor/cel.dev/cel-go/cel/async/async.go @@ -0,0 +1,235 @@ +// 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 async provides helpers for configuring and executing asynchronous CEL functions, +// including drain strategies, retry, timeout, concurrency limiting, and caching wrappers. +package async + +import ( + "context" + "errors" + "time" + + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" +) + +// Call describes a pending or completed asynchronous function call. +// This interface exposes a safe, read-only view of the internal interpreter state. +type Call = interpreter.AsyncCall + +// Observer provides callbacks for monitoring the lifecycle of asynchronous function calls. +// +// Implementations must be safe for concurrent use: the start and finish callbacks run on different +// goroutines, and finish callbacks for distinct calls may run concurrently. See +// interpreter.AsyncObserver for details. +type Observer = interpreter.AsyncObserver + +// BlockingOp is a blocking asynchronous function operation. +type BlockingOp = functions.BlockingAsyncOp + +// DrainAction dictates what ConcurrentEval should do after inspecting completions. +type DrainAction struct { + // Reevaluate indicates that the AST should be re-evaluated immediately. + // If true, WaitDuration is ignored. + Reevaluate bool + // WaitDuration indicates how long the evaluator should wait for additional + // completions before deciding to re-evaluate. A duration of 0 means wait + // indefinitely (block on the next completion). + WaitDuration time.Duration +} + +// DrainStrategy controls when ConcurrentEval re-evaluates after async completions. +// +// The evaluator consults the strategy each time a completion is received. +type DrainStrategy interface { + // NextAction evaluates the current state of asynchronous evaluation and + // determines the next step. + // + // - completed: The set of completions accumulated in the current batch. + // - active: The number of async calls currently launched but unresolved. + NextAction(completed []Call, active int) DrainAction +} + +// DrainNone returns a strategy that re-evaluates after every single completion. +// This is the default strategy. +func DrainNone() DrainStrategy { + return drainNone{} +} + +type drainNone struct{} + +func (drainNone) NextAction(completed []Call, active int) DrainAction { + return DrainAction{Reevaluate: active == 0 || len(completed) > 0} +} + +// DrainReady returns a strategy that waits for a short duration after the first +// completion to batch any other functions that complete at roughly the same time. +func DrainReady(debounce time.Duration) DrainStrategy { + return drainReady{debounce: debounce} +} + +type drainReady struct { + debounce time.Duration +} + +func (d drainReady) NextAction(completed []Call, active int) DrainAction { + if active == 0 { + return DrainAction{Reevaluate: true} // Nothing left to wait for + } + if len(completed) == 0 { + return DrainAction{Reevaluate: false, WaitDuration: 0} // Wait indefinitely for first + } + return DrainAction{Reevaluate: false, WaitDuration: d.debounce} // Wait for debounce period +} + +// DrainAll returns a strategy that waits for all currently pending calls to +// complete before re-evaluating. +// +// Note: This strategy is optimal for independent async calls, but will over-wait +// if some calls depend on the results of others. +func DrainAll() DrainStrategy { + return drainAll{} +} + +type drainAll struct{} + +func (drainAll) NextAction(completed []Call, active int) DrainAction { + return DrainAction{Reevaluate: active == 0} +} + +// Timeout wraps a BlockingAsyncOp with a per-call timeout. +// +// The timeout is enforced even when the wrapped function ignores its context: the function runs on +// its own goroutine and Timeout selects on the deadline, returning a timeout error when it +// fires. A function that ignores cancellation cannot be forcibly stopped (Go cannot kill a +// goroutine), so its goroutine continues running in the background until it returns on its own; +// only its result is abandoned. This is the recommended way to bound functions that may hang or +// are not under the caller's control. The extra goroutine is incurred only by Timeout-wrapped +// calls, not by async evaluation in general. +func Timeout(fn functions.BlockingAsyncOp, timeout time.Duration) functions.BlockingAsyncOp { + return func(ctx context.Context, args ...ref.Val) ref.Val { + tCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + resCh := make(chan ref.Val, 1) + go func() { resCh <- fn(tCtx, args...) }() + select { + case res := <-resCh: + return res + case <-tCtx.Done(): + return types.NewErr("operation timed out after %v: %v", timeout, tCtx.Err()) + } + } +} + +// TimeoutBinding wraps a BlockingAsyncOp with a per-call timeout and returns an OverloadOpt. +func TimeoutBinding(fn functions.BlockingAsyncOp, timeout time.Duration) decls.OverloadOpt { + return decls.AsyncBinding(Timeout(fn, timeout)) +} + +// RetryOption configures the behavior of RetryBinding. +type RetryOption func(*retryConfig) + +type retryConfig struct { + maxAttempts int + backoff time.Duration +} + +// RetryAttempts sets the maximum number of attempts (including the first one). +func RetryAttempts(attempts int) RetryOption { + return func(c *retryConfig) { + c.maxAttempts = attempts + } +} + +// RetryBackoff sets the fixed backoff duration between attempts. +func RetryBackoff(backoff time.Duration) RetryOption { + return func(c *retryConfig) { + c.backoff = backoff + } +} + +// RetryableError is an interface that errors can implement to signal whether they are retryable. +type RetryableError interface { + error + IsRetryable() bool +} + +// Retry wraps a BlockingAsyncOp with a retry policy. +// It will retry the operation if it returns a types.Err that wraps a RetryableError returning true for IsRetryable. +func Retry(fn functions.BlockingAsyncOp, opts ...RetryOption) functions.BlockingAsyncOp { + config := &retryConfig{ + maxAttempts: 3, + backoff: 100 * time.Millisecond, + } + for _, opt := range opts { + opt(config) + } + + return func(ctx context.Context, args ...ref.Val) ref.Val { + var lastErr ref.Val + var backoff *time.Timer + defer func() { + if backoff != nil { + backoff.Stop() + } + }() + for i := 0; i < config.maxAttempts; i++ { + if i > 0 { + // Reuse a single timer across attempts and stop it on cancellation so the + // pending timer is not left to fire after the call returns. + if backoff == nil { + backoff = time.NewTimer(config.backoff) + } else { + backoff.Reset(config.backoff) + } + select { + case <-backoff.C: + case <-ctx.Done(): + backoff.Stop() + return types.NewErr("operation cancelled during retry: %v", ctx.Err()) + } + } + + res := fn(ctx, args...) + if !types.IsError(res) { + return res + } + + err := res.(*types.Err) + lastErr = res + + if !isRetryable(err) { + return res + } + } + return lastErr + } +} + +// RetryBinding wraps a BlockingAsyncOp with a retry policy and returns an OverloadOpt. +func RetryBinding(fn functions.BlockingAsyncOp, opts ...RetryOption) decls.OverloadOpt { + return decls.AsyncBinding(Retry(fn, opts...)) +} + +func isRetryable(err *types.Err) bool { + var re RetryableError + if errors.As(err, &re) { + return re.IsRetryable() + } + return false +} diff --git a/vendor/github.com/google/cel-go/cel/cel.go b/vendor/cel.dev/cel-go/cel/cel.go similarity index 54% rename from vendor/github.com/google/cel-go/cel/cel.go rename to vendor/cel.dev/cel-go/cel/cel.go index eb5a9f4cc52..9ba957a7e31 100644 --- a/vendor/github.com/google/cel-go/cel/cel.go +++ b/vendor/cel.dev/cel-go/cel/cel.go @@ -17,3 +17,24 @@ // CEL is a non-Turing complete expression language designed to parse, check, and evaluate // expressions against user-defined environments. package cel + +// Compile is a convenience function that constructs a new Env using the provided EnvOption values, +// compiles the expression string, and plans an executable Program. +// +// Warning: Creating a new environment for every compilation is expensive. Environment setup should be done once +// and shared across expression compilations when the options remain the same. +func Compile(expression string, opts ...EnvOption) (Program, error) { + env, err := NewEnv(opts...) + if err != nil { + return nil, err + } + ast, iss := env.Compile(expression) + if iss.Err() != nil { + return nil, iss.Err() + } + prg, err := env.Program(ast, EvalOptions(OptOptimize)) + if err != nil { + return nil, err + } + return prg, nil +} diff --git a/vendor/github.com/google/cel-go/cel/decls.go b/vendor/cel.dev/cel-go/cel/decls.go similarity index 89% rename from vendor/github.com/google/cel-go/cel/decls.go rename to vendor/cel.dev/cel-go/cel/decls.go index 4d4873bd6b1..8a01116c7b6 100644 --- a/vendor/github.com/google/cel-go/cel/decls.go +++ b/vendor/cel.dev/cel-go/cel/decls.go @@ -17,11 +17,11 @@ package cel import ( "fmt" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" celpb "cel.dev/expr" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" @@ -220,7 +220,11 @@ func ExcludeOverloads(overloadIDs ...string) OverloadSelector { // FunctionDecls provides one or more fully formed function declarations to be added to the environment. func FunctionDecls(funcs ...*decls.FunctionDecl) EnvOption { return func(e *Env) (*Env, error) { + if len(funcs) == 0 { + return e, nil + } var err error + e.ensureMutableFunctions() for _, fn := range funcs { if existing, found := e.functions[fn.Name()]; found { fn, err = existing.Merge(fn) @@ -342,10 +346,39 @@ func FunctionBinding(binding functions.FunctionOp) OverloadOpt { // LateFunctionBinding indicates that the function has a binding which is not known at compile time. // This is useful for functions which have side-effects or are not deterministically computable. +// +// The implementation is supplied at evaluation time as a LateBoundFunction within an activation +// created by FunctionVars, and is subject to the same runtime type-guard as an eager binding. func LateFunctionBinding() OverloadOpt { return decls.LateFunctionBinding() } +// AsyncBinding provides the implementation of an asynchronous overload. The provided function +// is called in its own goroutine with the provided context. The function should block until +// the result is available, and the framework manages goroutine and channel lifecycle. +// +// This follows the same pattern used by gRPC-Go and other major Go frameworks where user +// code is synchronous and the framework manages concurrency. +// +// Context contract: the function MUST return promptly once its context is cancelled. The +// framework cannot forcibly terminate the goroutine running the function, so a function that +// ignores cancellation will leak its goroutine and hold a concurrency slot (see +// AsyncMaxConcurrency) until it returns on its own. For functions that may hang or that are not +// under your control, wrap them with async.TimeoutBinding to bound their runtime. +func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt { + return decls.AsyncBinding(fn) +} + +// SingletonAsyncBinding creates a singleton async function definition from a blocking function, +// to be used with all function overloads. The provided function is called in its own goroutine +// with the provided context. +// +// Note, this approach works well if operand is expected to have a specific trait which it implements, +// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings. +func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt { + return decls.SingletonAsyncBinding(fn, traits...) +} + // OverloadIsNonStrict enables the function to be called with error and unknown argument values. // // Note: do not use this option unless absoluately necessary as it should be an uncommon feature. diff --git a/vendor/github.com/google/cel-go/cel/env.go b/vendor/cel.dev/cel-go/cel/env.go similarity index 77% rename from vendor/github.com/google/cel-go/cel/env.go rename to vendor/cel.dev/cel-go/cel/env.go index 7139e415eab..72d56e4dfdc 100644 --- a/vendor/github.com/google/cel-go/cel/env.go +++ b/vendor/cel.dev/cel-go/cel/env.go @@ -17,24 +17,26 @@ package cel import ( "errors" "fmt" + "maps" "math" "slices" "strings" "sync" - "github.com/google/cel-go/checker" - chkdecls "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common" - celast "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/env" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/stdlib" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/checker" + chkdecls "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common" + celast "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/env" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/stdlib" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" + "cel.dev/cel-go/parser" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "google.golang.org/protobuf/reflect/protoreflect" @@ -48,6 +50,10 @@ type Source = common.Source type Ast struct { source Source impl *celast.AST + // loadErr captures an error detected while loading the AST (e.g. an over-deep AST ingested via + // ParsedExprToAst / CheckedExprToAst) so it can be surfaced when the Ast is checked or planned + // instead of recursing into the checker or planner on adversarially deep input. + loadErr error } // NativeRep converts the AST to a Go-native representation. @@ -144,10 +150,28 @@ type Env struct { limits map[limitID]int libraries map[string]SingletonLibrary validators []ASTValidator - costOptions []checker.CostOption + costOptions []cost.Option + costConfig *costConfig + costModel costModelOptions - funcBindOnce sync.Once - functionBindings []*functions.Overload + // Flags for copy-on-write behavior with env.Extend. + funcsShared bool + featuresShared bool + appliedFeaturesShared bool + limitsShared bool + libsShared bool + + parent *Env + + // sharedDispatcher caches a dispatcher populated with the env's function + // bindings, built once and reused across every Program() constructed from + // this env. It is read-only after construction; each Program layers a thin + // child over it for per-program Functions(). Extended envs reuse the parent's + // dispatcher if functions are unchanged. + sharedDispatcher interpreter.Dispatcher + dispOnce sync.Once + hasAsync bool + dispErr error // Internal parser representation prsr *parser.Parser @@ -164,6 +188,60 @@ type Env struct { progOpts []ProgramOption } +// costConfig accumulates the cost options while the environment is configured. Env.configure +// builds the environment's costModelOptions from it once the options have run; it is kept only so +// that Env.Extend can configure a copy. +type costConfig struct { + strategy cost.SizingStrategy + models []cost.OverloadModel + + // version pins the cost model revision used for estimation and tracking. + // + // A pointer rather than a value because version 0 is a meaningful pin, so the + // nil case is the only way to express "caller did not ask for a revision". + version *uint32 +} + +func (c *costConfig) copy() *costConfig { + return &costConfig{ + strategy: c.strategy, + models: slices.Clone(c.models), + version: c.version, + } +} + +// build derives the estimation and tracking options. A cost.ModelOption serves both, so the two +// lists hold the same options. +func (c *costConfig) build() costModelOptions { + var opts []cost.ModelOption + if len(c.models) > 0 { + opts = append(opts, cost.Model(c.models...)) + } + if c.strategy != nil { + opts = append(opts, cost.WithSizingStrategy(c.strategy)) + } + if c.version != nil { + opts = append(opts, cost.ModelVersion(*c.version)) + } + built := costModelOptions{ + estimate: make([]cost.Option, len(opts)), + tracker: make([]cost.TrackerOption, len(opts)), + } + for i, opt := range opts { + built.estimate[i] = opt + built.tracker[i] = opt + } + return built +} + +// costModelOptions are the options which apply the environment's cost models, sizing strategy, and +// revision to estimation and tracking. They precede the caller's options so the caller may +// override them. +type costModelOptions struct { + estimate []cost.Option + tracker []cost.TrackerOption +} + // ToConfig produces a YAML-serializable env.Config object from the given environment. // // The serialized configuration value is intended to represent a baseline set of config @@ -199,8 +277,14 @@ func (e *Env) ToConfig(name string) (*env.Config, error) { // Track the options which have been configured by a library and // then diff the library version against the configured function // to detect incremental overloads or rewrites. - libEnv, _ := NewCustomEnv() - libEnv, _ = Lib(lib)(libEnv) + libEnv, err := NewCustomEnv() + if err != nil { + return nil, err + } + libEnv, err = Lib(lib)(libEnv) + if err != nil { + return nil, err + } for fnName, fnDecl := range libEnv.Functions() { if len(fnDecl.OverloadDecls()) == 0 { continue @@ -361,25 +445,25 @@ func NewEnv(opts ...EnvOption) (*Env, error) { // See the EnvOption helper functions for the options that can be used to configure the // environment. func NewCustomEnv(opts ...EnvOption) (*Env, error) { - registry, err := types.NewProtoRegistry() + registry, err := types.NewRegistry() if err != nil { return nil, err } return (&Env{ - variables: []*decls.VariableDecl{}, - functions: map[string]*decls.FunctionDecl{}, - functionBindings: []*functions.Overload{}, - macros: []parser.Macro{}, - Container: containers.DefaultContainer, - adapter: registry, - provider: registry, - features: map[int]bool{}, - appliedFeatures: map[int]bool{}, - limits: map[limitID]int{}, - libraries: map[string]SingletonLibrary{}, - validators: []ASTValidator{}, - progOpts: []ProgramOption{}, - costOptions: []checker.CostOption{}, + variables: []*decls.VariableDecl{}, + functions: map[string]*decls.FunctionDecl{}, + macros: []parser.Macro{}, + Container: containers.DefaultContainer, + adapter: registry, + provider: registry, + features: map[int]bool{}, + appliedFeatures: map[int]bool{}, + limits: map[limitID]int{}, + libraries: map[string]SingletonLibrary{}, + validators: []ASTValidator{}, + progOpts: []ProgramOption{}, + costOptions: []cost.Option{}, + costConfig: &costConfig{}, }).configure(opts) } @@ -395,6 +479,20 @@ func NewCustomEnv(opts ...EnvOption) (*Env, error) { // It is possible to have both non-nil Ast and Issues values returned from this call: however, // the mere presence of an Ast does not imply that it is valid for use. func (e *Env) Check(ast *Ast) (*Ast, *Issues) { + // Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by + // ParsedExprToAst / CheckedExprToAst) before recursing into the type checker on it. + if ast != nil && ast.loadErr != nil { + errs := common.NewErrors(ast.Source()) + errs.ReportErrorString(common.NoLocation, ast.loadErr.Error()) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) + } + if nodeLimit := e.configuredExpressionNodeLimit(); nodeLimit > 0 && ast != nil && ast.NativeRep() != nil { + if count := celast.NodeCount(ast.NativeRep()); count > nodeLimit { + errs := common.NewErrors(ast.Source()) + errs.ReportErrorString(common.NoLocation, fmt.Sprintf("expression node count exceeds limit: count %d, limit %d", count, nodeLimit)) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) + } + } // Construct the internal checker env, erroring if there is an issue adding the declarations. chk, err := e.initChecker() if err != nil { @@ -445,6 +543,15 @@ func (e *Env) configuredExpressionSizeLimit() int { return 100_000 } +// configuredExpressionNodeLimit returns the effective expression node limit. +// A zero value means "use default". +func (e *Env) configuredExpressionNodeLimit() int { + if l := e.limits[limitExpressionNodeCount]; l != 0 { + return l + } + return 100_000 +} + // Compile combines the Parse and Check phases CEL program compilation to produce an Ast and // associated issues. // @@ -489,34 +596,17 @@ func (e *Env) CompileSource(src Source) (*Ast, *Issues) { // TypeProvider are immutable, or that their underlying implementations are based on the // ref.TypeRegistry which provides a Copy method which will be invoked by this method. func (e *Env) Extend(opts ...EnvOption) (*Env, error) { - chk, chkErr := e.getCheckerOrError() - if chkErr != nil { + if _, chkErr := e.getCheckerOrError(); chkErr != nil { return nil, chkErr } - prsrOptsCopy := make([]parser.Option, len(e.prsrOpts)) - copy(prsrOptsCopy, e.prsrOpts) - - // The type-checker is configured with Declarations. The declarations may either be provided - // as options which have not yet been validated, or may come from a previous checker instance - // whose types have already been validated. - chkOptsCopy := make([]checker.Option, len(e.chkOpts)) - copy(chkOptsCopy, e.chkOpts) - - // Copy the declarations if needed. - if chk != nil { - // If the type-checker has already been instantiated, then the e.declarations have been - // validated within the chk instance. - chkOptsCopy = append(chkOptsCopy, checker.ValidatedDeclarations(chk)) - } - varsCopy := make([]*decls.VariableDecl, len(e.variables)) - copy(varsCopy, e.variables) - - // Copy macros and program options - macsCopy := make([]parser.Macro, len(e.macros)) - progOptsCopy := make([]ProgramOption, len(e.progOpts)) - copy(macsCopy, e.macros) - copy(progOptsCopy, e.progOpts) + prsrOptsCopy := slices.Clone(e.prsrOpts) + chkOptsCopy := slices.Clone(e.chkOpts) + varsCopy := slices.Clone(e.variables) + macsCopy := slices.Clone(e.macros) + progOptsCopy := slices.Clone(e.progOpts) + validatorsCopy := slices.Clone(e.validators) + costOptsCopy := slices.Clone(e.costOptions) // Copy the adapter / provider if they appear to be mutable. adapter := e.adapter @@ -545,53 +635,70 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) { adapter = adapterReg.Copy() } - featuresCopy := make(map[int]bool, len(e.features)) - for k, v := range e.features { - featuresCopy[k] = v - } - appliedFeaturesCopy := make(map[int]bool, len(e.appliedFeatures)) - for k, v := range e.appliedFeatures { - appliedFeaturesCopy[k] = v - } - limitsCopy := make(map[limitID]int, len(e.limits)) - for k, v := range e.limits { - limitsCopy[k] = v - } - funcsCopy := make(map[string]*decls.FunctionDecl, len(e.functions)) - for k, v := range e.functions { - funcsCopy[k] = v - } - libsCopy := make(map[string]SingletonLibrary, len(e.libraries)) - for k, v := range e.libraries { - libsCopy[k] = v - } - validatorsCopy := make([]ASTValidator, len(e.validators)) - copy(validatorsCopy, e.validators) - - costOptsCopy := make([]checker.CostOption, len(e.costOptions)) - copy(costOptsCopy, e.costOptions) - ext := &Env{ + parent: e, Container: e.Container, variables: varsCopy, - functions: funcsCopy, + functions: e.functions, macros: macsCopy, contextProto: e.contextProto, progOpts: progOptsCopy, adapter: adapter, - features: featuresCopy, - limits: limitsCopy, - appliedFeatures: appliedFeaturesCopy, - libraries: libsCopy, + features: e.features, + limits: e.limits, + appliedFeatures: e.appliedFeatures, + libraries: e.libraries, validators: validatorsCopy, provider: provider, chkOpts: chkOptsCopy, prsrOpts: prsrOptsCopy, costOptions: costOptsCopy, + costConfig: e.costConfig.copy(), + // Copy-on-write flags. + funcsShared: true, + featuresShared: true, + limitsShared: true, + appliedFeaturesShared: true, + libsShared: true, } return ext.configure(opts) } +func (e *Env) ensureMutableFunctions() { + if e.funcsShared { + e.functions = maps.Clone(e.functions) + e.funcsShared = false + } +} + +func (e *Env) ensureMutableLibraries() { + if e.libsShared { + e.libraries = maps.Clone(e.libraries) + e.libsShared = false + } +} + +func (e *Env) ensureMutableFeatures() { + if e.featuresShared { + e.features = maps.Clone(e.features) + e.featuresShared = false + } +} + +func (e *Env) ensureMutableAppliedFeatures() { + if e.appliedFeaturesShared { + e.appliedFeatures = maps.Clone(e.appliedFeatures) + e.appliedFeaturesShared = false + } +} + +func (e *Env) ensureMutableLimits() { + if e.limitsShared { + e.limits = maps.Clone(e.limits) + e.limitsShared = false + } +} + // HasFeature checks whether the environment enables the given feature // flag, as enumerated in options.go. func (e *Env) HasFeature(flag int) bool { @@ -622,25 +729,17 @@ func (e *Env) HasFunction(functionName string) bool { // Functions returns a shallow copy of the Functions, keyed by function name, that have been configured in the environment. func (e *Env) Functions() map[string]*decls.FunctionDecl { - shallowCopy := make(map[string]*decls.FunctionDecl, len(e.functions)) - for nm, fn := range e.functions { - shallowCopy[nm] = fn - } - return shallowCopy + return maps.Clone(e.functions) } // Variables returns a shallow copy of the variables associated with the environment. func (e *Env) Variables() []*decls.VariableDecl { - shallowCopy := make([]*decls.VariableDecl, len(e.variables)) - copy(shallowCopy, e.variables) - return shallowCopy + return slices.Clone(e.variables) } // Macros returns a shallow copy of macros associated with the environment. func (e *Env) Macros() []Macro { - shallowCopy := make([]Macro, len(e.macros)) - copy(shallowCopy, e.macros) - return shallowCopy + return slices.Clone(e.macros) } // HasValidator returns whether a specific ASTValidator has been configured in the environment. @@ -653,9 +752,9 @@ func (e *Env) HasValidator(name string) bool { return false } -// Validators returns the set of ASTValidators configured on the environment. +// Validators returns a shallow copy of the set of ASTValidators configured on the environment. func (e *Env) Validators() []ASTValidator { - return e.validators[:] + return slices.Clone(e.validators) } // Parse parses the input expression value `txt` to a Ast and/or a set of Issues. @@ -687,6 +786,12 @@ func (e *Env) ParseSource(src Source) (*Ast, *Issues) { // Program generates an evaluable instance of the Ast within the environment (Env). func (e *Env) Program(ast *Ast, opts ...ProgramOption) (Program, error) { + // Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by + // ParsedExprToAst / CheckedExprToAst) rather than recursing into the planner on it. This is a + // cheap field read; the depth traversal itself runs once at conversion time, not here. + if ast != nil && ast.loadErr != nil { + return nil, ast.loadErr + } return e.PlanProgram(ast.NativeRep(), opts...) } @@ -703,6 +808,41 @@ func (e *Env) PlanProgram(a *celast.AST, opts ...ProgramOption) (Program, error) return newProgram(e, a, optSet) } +func (e *Env) initDispatcher() (interpreter.Dispatcher, bool, error) { + e.dispOnce.Do(func() { + if e.parent != nil && e.funcsShared { + // The dispatcher setup is skipped when the child has mutated the function set. + // As the child function set contains a copy of all parent function declarations + // by virtue of copy on write semantics. + d, hasAsync, err := e.parent.initDispatcher() + e.sharedDispatcher = d + e.hasAsync = hasAsync + e.dispErr = err + return + } + hasAsync := false + var bindings []*functions.Overload + for _, fn := range e.functions { + bs, err := fn.Bindings() + if err != nil { + e.dispErr = err + return + } + for _, b := range bs { + if b.Async != nil { + hasAsync = true + } + } + bindings = append(bindings, bs...) + } + d := interpreter.NewDispatcher() + e.dispErr = d.Add(bindings...) + e.sharedDispatcher = d + e.hasAsync = hasAsync + }) + return e.sharedDispatcher, e.hasAsync, e.dispErr +} + // CELTypeAdapter returns the `types.Adapter` configured for the environment. func (e *Env) CELTypeAdapter() types.Adapter { return e.adapter @@ -780,6 +920,12 @@ func (e *Env) PartialVars(vars any) (PartialActivation, error) { // // See the PartialVars helper for how to construct a PartialActivation. // +// Note: if the original Ast contains a comprehension macro (e.g. all, exists, exists_one, +// filter, map) whose range or predicate cannot be fully resolved by partial evaluation, the +// Env must have been created with the EnableMacroCallTracking() option. Without it, this method +// returns an error when it cannot re-serialize the unresolved comprehension's expanded internal +// representation back into CEL source syntax. +// // TODO: Consider adding an option to generate a Program.Residual to avoid round-tripping to an // Ast format and then Program again. func (e *Env) ResidualAst(a *Ast, details *EvalDetails) (*Ast, error) { @@ -806,11 +952,13 @@ func (e *Env) ResidualAst(a *Ast, details *EvalDetails) (*Ast, error) { // EstimateCost estimates the cost of a type checked CEL expression using the length estimates of input data and // extension functions provided by estimator. -func (e *Env) EstimateCost(ast *Ast, estimator checker.CostEstimator, opts ...checker.CostOption) (checker.CostEstimate, error) { - extendedOpts := make([]checker.CostOption, 0, len(e.costOptions)) - extendedOpts = append(extendedOpts, opts...) +func (e *Env) EstimateCost(ast *Ast, estimator cost.Estimator, opts ...cost.CostOption) (cost.CostEstimate, error) { + modelOpts := e.costModel.estimate + extendedOpts := make([]cost.Option, 0, len(modelOpts)+len(e.costOptions)+len(opts)) + extendedOpts = append(extendedOpts, modelOpts...) extendedOpts = append(extendedOpts, e.costOptions...) - return checker.Cost(ast.NativeRep(), estimator, extendedOpts...) + extendedOpts = append(extendedOpts, opts...) + return cost.Cost(ast.NativeRep(), estimator, extendedOpts...) } // configure applies a series of EnvOptions to the current environment. @@ -828,6 +976,7 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) { // If the default UTC timezone has been disabled, configure the legacy overloads if utcTime, isSet := e.features[featureDefaultUTCTimeZone]; isSet && !utcTime { if !e.appliedFeatures[featureDefaultUTCTimeZone] { + e.ensureMutableAppliedFeatures() e.appliedFeatures[featureDefaultUTCTimeZone] = true e, err = Lib(timeLegacyLibrary{})(e) if err != nil { @@ -836,6 +985,8 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) { } } + e.costModel = e.costConfig.build() + // Configure the parser. prsrOpts := []parser.Option{} prsrOpts = append(prsrOpts, e.prsrOpts...) @@ -859,6 +1010,9 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) { if l := e.limits[limitParseRecursionDepth]; l != 0 { prsrOpts = append(prsrOpts, parser.MaxRecursionDepth(l)) } + if l := e.limits[limitExpressionNodeCount]; l != 0 { + prsrOpts = append(prsrOpts, parser.MaxExpressionNodeCount(l)) + } e.prsr, err = parser.NewParser(prsrOpts...) if err != nil { return nil, err @@ -897,6 +1051,48 @@ func (e *Env) initChecker() (*checker.Env, error) { chkOpts = append(chkOpts, checker.JSONFieldNames(e.HasFeature(featureJSONFieldNames))) + chkOpts = append(chkOpts, checker.Catalog(func() *env.Catalog { + conf, err := e.ToConfig("") + if err != nil { + return env.NewCatalog() + } + cat := conf.ToCatalog() + for _, f := range e.functions { + cat.Add(&env.CatalogSymbol{ + Name: f.Name(), + Kind: env.FunctionKind, + }) + } + for _, t := range stdlib.Types() { + cat.Add(&env.CatalogSymbol{ + Name: t.Name(), + Kind: env.TypeKind, + }) + } + for _, m := range e.macros { + cat.Add(&env.CatalogSymbol{ + Name: m.Function(), + Kind: env.MacroKind, + }) + } + // TODO: Come up with a better mechanism to dynamically populate catalog symbols + // for un-enabled libraries (e.g. leaning on the introspection and serialization + // properties used by ToConfig). + if !e.HasLibrary("cel.lib.optional") { + cat.Add((&optionalLib{}).CatalogSymbols()...) + } + return cat + })) + + if e.parent != nil && e.funcsShared { + parentChk, err := e.parent.initChecker() + if err != nil { + e.setCheckerOrError(nil, err) + return + } + chkOpts = append(chkOpts, checker.ValidatedDeclarations(parentChk)) + } + ce, err := checker.NewEnv(e.Container, e.provider, chkOpts...) if err != nil { e.setCheckerOrError(nil, err) @@ -909,14 +1105,16 @@ func (e *Env) initChecker() (*checker.Env, error) { return } // Add the function declarations which are derived from the FunctionDecl instances. - for _, fn := range e.functions { - if fn.IsDeclarationDisabled() { - continue - } - err = ce.AddFunctions(fn) - if err != nil { - e.setCheckerOrError(nil, err) - return + if e.parent == nil || !e.funcsShared { + for _, fn := range e.functions { + if fn.IsDeclarationDisabled() { + continue + } + err = ce.AddFunctions(fn) + if err != nil { + e.setCheckerOrError(nil, err) + return + } } } // Add function declarations here separately. diff --git a/vendor/github.com/google/cel-go/cel/fieldpaths.go b/vendor/cel.dev/cel-go/cel/fieldpaths.go similarity index 98% rename from vendor/github.com/google/cel-go/cel/fieldpaths.go rename to vendor/cel.dev/cel-go/cel/fieldpaths.go index 570fce3a465..b63c2ba2ac9 100644 --- a/vendor/github.com/google/cel-go/cel/fieldpaths.go +++ b/vendor/cel.dev/cel-go/cel/fieldpaths.go @@ -4,8 +4,8 @@ import ( "slices" "strings" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/types" ) // fieldPath represents a selection path to a field from a variable in a CEL environment. diff --git a/vendor/github.com/google/cel-go/cel/folding.go b/vendor/cel.dev/cel-go/cel/folding.go similarity index 66% rename from vendor/github.com/google/cel-go/cel/folding.go rename to vendor/cel.dev/cel-go/cel/folding.go index 0cff9517a44..494f38dac1d 100644 --- a/vendor/github.com/google/cel-go/cel/folding.go +++ b/vendor/cel.dev/cel-go/cel/folding.go @@ -15,14 +15,16 @@ package cel import ( + "context" + "errors" "fmt" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // ConstantFoldingOption defines a functional option for configuring constant folding. @@ -93,18 +95,18 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) for _, fold := range foldableExprs { // If the expression could be folded because it's a non-strict call, and the // branches are pruned, continue to the next fold. - if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, fold) { + if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, a, fold) { continue } - // Late-bound function calls cannot be folded. - if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, fold) { + // Async and late-bound function calls cannot be folded. + if fold.Kind() == ast.CallKind && (isLateBoundFunctionCall(ctx, fold) || isAsyncFunctionCall(ctx, fold)) { continue } // Otherwise, assume all context is needed to evaluate the expression. err := opt.tryFold(ctx, a, fold) - // Ignore errors for identifiers, since there is no guarantee that the environment + // Ignore errors for identifiers or subexpressions that cannot be folded, since there is no guarantee that the environment // has a value for them. - if err != nil && fold.Kind() != ast.IdentKind { + if err != nil && fold.Kind() != ast.IdentKind && !errors.Is(err, errCannotFold) { ctx.ReportErrorAtID(fold.ID(), "constant-folding evaluation failed: %v", err.Error()) return a } @@ -142,32 +144,52 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) return a } +var errCannotFold = errors.New("subexpression cannot be folded") + // tryFold attempts to evaluate a sub-expression to a literal. // // If the evaluation succeeds, the input expr value will be modified to become a literal, otherwise // the method will return an error. func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) error { - // Assume all context is needed to evaluate the expression. - subAST := &Ast{ - impl: ast.NewCheckedAST(ast.NewAST(expr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()), - } - prg, err := ctx.Program(subAST) - if err != nil { - return err - } activation := opt.knownValues if activation == nil { activation = NoVars() } - out, _, err := prg.Eval(activation) + navExpr := expr.(ast.NavigableExpr) + out, err := evaluateExpr(ctx, a, navExpr, activation) if err != nil { return err } // Update the fold expression to be a literal. ctx.UpdateExpr(expr, ctx.NewLiteral(out)) + delete(a.ReferenceMap(), expr.ID()) return nil } +func evaluateExpr(ctx *OptimizerContext, a *ast.AST, navigableExpr ast.NavigableExpr, activation Activation) (ref.Val, error) { + partialActivation, err := ctx.PartialVars(activation) + if err != nil { + return nil, err + } + subAST := &Ast{ + impl: ast.NewCheckedAST(ast.NewAST(navigableExpr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()), + } + prg, err := ctx.Program(subAST) + if err != nil { + return nil, errCannotFold + } + // Folding will not attempt to call async functions, + // but the presence of such functions requires the use of `ConcurrentEval` in order to + // avoid an early return error which blocks async functions from running in `Eval` and + // `ContextEval` call paths. + resCh := prg.ConcurrentEval(context.Background(), partialActivation) + res := <-resCh + if res.Err != nil || types.IsUnknown(res.Val) { + return nil, errCannotFold + } + return res.Val, nil +} + func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool { call := expr.AsCall() function := ctx.Functions()[call.FunctionName()] @@ -177,16 +199,25 @@ func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool { return function.HasLateBinding() } +func isAsyncFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool { + call := expr.AsCall() + function := ctx.Functions()[call.FunctionName()] + if function == nil { + return false + } + return function.IsAsync() +} + // maybePruneBranches inspects the non-strict call expression to determine whether // a branch can be removed. Evaluation will naturally prune logical and / or calls, // but conditional will not be pruned cleanly, so this is one small area where the // constant folding step reimplements a portion of the evaluator. -func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { +func maybePruneBranches(ctx *OptimizerContext, a *ast.AST, expr ast.NavigableExpr) bool { call := expr.AsCall() args := call.Args() switch call.FunctionName() { case operators.LogicalAnd, operators.LogicalOr: - return maybeShortcircuitLogic(ctx, call.FunctionName(), args, expr) + return maybeShortcircuitLogic(ctx, a, call.FunctionName(), args, expr) case operators.Conditional: cond := args[0] truthy := args[1] @@ -196,32 +227,50 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { } if cond.AsLiteral() == types.True { ctx.UpdateExpr(expr, truthy) + updateMetadata(a, expr.ID(), truthy.ID()) } else { ctx.UpdateExpr(expr, falsy) + updateMetadata(a, expr.ID(), falsy.ID()) } return true case operators.In: haystack := args[1] - if haystack.Kind() == ast.ListKind && haystack.AsList().Size() == 0 { + if isCollectionEmpty(haystack) { ctx.UpdateExpr(expr, ctx.NewLiteral(types.False)) + delete(a.ReferenceMap(), expr.ID()) return true } - needle := args[0] - if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind { - needleValue := needle.AsLiteral() - list := haystack.AsList() - for _, e := range list.Elements() { - if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True { - ctx.UpdateExpr(expr, ctx.NewLiteral(types.True)) - return true - } + if collectionContainsNeedle(args[0], haystack) { + ctx.UpdateExpr(expr, ctx.NewLiteral(types.True)) + delete(a.ReferenceMap(), expr.ID()) + return true + } + case operators.Add: + if len(args) == 2 && args[0].Kind() == ast.ListKind && args[1].Kind() == ast.ListKind { + leftList := args[0].AsList() + rightList := args[1].AsList() + + elems := make([]ast.Expr, 0, leftList.Size()+rightList.Size()) + elems = append(elems, leftList.Elements()...) + elems = append(elems, rightList.Elements()...) + + optIndices := make([]int32, 0, len(leftList.OptionalIndices())+len(rightList.OptionalIndices())) + optIndices = append(optIndices, leftList.OptionalIndices()...) + offset := int32(leftList.Size()) + for _, idx := range rightList.OptionalIndices() { + optIndices = append(optIndices, offset+idx) } + + combinedList := ctx.NewList(elems, optIndices) + ctx.UpdateExpr(expr, combinedList) + delete(a.ReferenceMap(), expr.ID()) + return true } } return false } -func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.Expr, expr ast.NavigableExpr) bool { +func maybeShortcircuitLogic(ctx *OptimizerContext, a *ast.AST, function string, args []ast.Expr, expr ast.NavigableExpr) bool { shortcircuit := types.False skip := types.True if function == operators.LogicalOr { @@ -239,22 +288,52 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E } if arg.AsLiteral() == shortcircuit { ctx.UpdateExpr(expr, arg) + updateMetadata(a, expr.ID(), arg.ID()) return true } } if len(newArgs) == 0 { newArgs = append(newArgs, args[0]) - ctx.UpdateExpr(expr, newArgs[0]) - return true + } + if len(newArgs) == len(args) { + return false } if len(newArgs) == 1 { + if !isBoolType(a, newArgs[0]) { + return false + } ctx.UpdateExpr(expr, newArgs[0]) + updateMetadata(a, expr.ID(), newArgs[0].ID()) return true } ctx.UpdateExpr(expr, ctx.NewCall(function, newArgs...)) return true } +func updateMetadata(a *ast.AST, targetID, updatedID int64) { + if a == nil { + return + } + if ref, found := a.ReferenceMap()[updatedID]; found { + a.ReferenceMap()[targetID] = ref + } else { + delete(a.ReferenceMap(), targetID) + } + if t, found := a.TypeMap()[updatedID]; found { + a.TypeMap()[targetID] = t + } +} + +func isBoolType(a *ast.AST, e ast.Expr) bool { + if a != nil && a.GetType(e.ID()) == types.BoolType { + return true + } + if e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == types.BoolType { + return true + } + return false +} + // pruneOptionalElements works from the bottom up to resolve optional elements within // aggregate literals. // @@ -285,9 +364,9 @@ func pruneOptionalListElements(ctx *OptimizerContext, e ast.Expr) { updatedElems := []ast.Expr{} updatedIndices := []int32{} newOptIndex := -1 - for _, e := range elems { + for i, e := range elems { newOptIndex++ - if !l.IsOptional(int32(newOptIndex)) { + if !l.IsOptional(int32(i)) { updatedElems = append(updatedElems, e) continue } @@ -501,7 +580,7 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext, sel := e.AsSelect() // guaranteed to be a navigable value return constantMatcher(sel.Operand().(ast.NavigableExpr)) case ast.IdentKind: - return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil + return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil && !hasComprehensionVar(e) case ast.ComprehensionKind: if isNestedComprehension(e) { return false @@ -513,12 +592,15 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext, nested := e.AsComprehension() vars[nested.AccuVar()] = true vars[nested.IterVar()] = true + if nested.IterVar2() != "" { + vars[nested.IterVar2()] = true + } } if e.Kind() == ast.IdentKind && !vars[e.AsIdent()] { constantExprs = false } - // Late-bound function calls cannot be folded. - if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, e) { + // Async and late-bound function calls cannot be folded. + if e.Kind() == ast.CallKind && (isLateBoundFunctionCall(ctx, e) || isAsyncFunctionCall(ctx, e)) { constantExprs = false } }) @@ -554,20 +636,26 @@ func constantCallMatcher(e ast.NavigableExpr) bool { return true } } - if fnName == operators.In { - haystack := children[1] - if haystack.Kind() == ast.ListKind && haystack.AsList().Size() == 0 { + if fnName == operators.Equals || fnName == operators.NotEquals { + if hasComprehensionVar(e) { + return false + } + if isExprConstantOfKind(children[0], types.BoolType) || isExprConstantOfKind(children[1], types.BoolType) { return true } - needle := children[0] - if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind { - needleValue := needle.AsLiteral() - list := haystack.AsList() - for _, e := range list.Elements() { - if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True { - return true - } - } + } + if fnName == operators.In { + if hasComprehensionVar(e) { + return false + } + haystack := children[1] + if isCollectionEmpty(haystack) || collectionContainsNeedle(children[0], haystack) { + return true + } + } + if fnName == operators.Add { + if len(children) == 2 && children[0].Kind() == ast.ListKind && children[1].Kind() == ast.ListKind { + return true } } // convert all other calls with constant arguments @@ -579,6 +667,138 @@ func constantCallMatcher(e ast.NavigableExpr) bool { return true } +func isCollectionEmpty(e ast.Expr) bool { + switch e.Kind() { + case ast.ListKind: + return e.AsList().Size() == 0 + case ast.MapKind: + return e.AsMap().Size() == 0 + default: + return false + } +} + +func exprEqualsNeedle(needle, target ast.Expr) bool { + if needle.Kind() == ast.LiteralKind && target.Kind() == ast.LiteralKind { + return target.AsLiteral().Equal(needle.AsLiteral()) == types.True + } + if needle.Kind() == ast.IdentKind && target.Kind() == ast.IdentKind { + return target.AsIdent() == needle.AsIdent() + } + return false +} + +func collectionContainsNeedle(needle, haystack ast.Expr) bool { + if needle.Kind() != ast.LiteralKind && !isSelfEqualIdent(needle) { + return false + } + switch haystack.Kind() { + case ast.ListKind: + list := haystack.AsList() + for i, elem := range list.Elements() { + if list.IsOptional(int32(i)) { + if needle.Kind() == ast.LiteralKind && elem.Kind() == ast.LiteralKind { + if opt, ok := elem.AsLiteral().(*types.Optional); ok && opt.HasValue() { + if opt.GetValue().Equal(needle.AsLiteral()) == types.True { + return true + } + } + } + continue + } + if exprEqualsNeedle(needle, elem) { + return true + } + } + case ast.MapKind: + for _, entryExpr := range haystack.AsMap().Entries() { + entry := entryExpr.AsMapEntry() + if entry.IsOptional() { + if val := entry.Value(); val.Kind() == ast.LiteralKind { + if opt, ok := val.AsLiteral().(*types.Optional); ok && opt.HasValue() { + if exprEqualsNeedle(needle, entry.Key()) { + return true + } + } + } + continue + } + if exprEqualsNeedle(needle, entry.Key()) { + return true + } + } + } + return false +} + +// isSelfEqualIdent indicates whether the expression is an identifier whose static type +// guarantees that its runtime value is equal to itself. +// +// Matching an identifier against a list element by name only proves list membership when the +// value the name resolves to is self-equal. A double may be NaN, which is not equal to itself, +// and dyn, abstract, and struct types may all hold a NaN at runtime, so the check is limited +// to the scalar types which cannot, and to the aggregate types whose type parameters are +// themselves self-equal. +func isSelfEqualIdent(e ast.Expr) bool { + if e.Kind() != ast.IdentKind { + return false + } + nav, ok := e.(ast.NavigableExpr) + if !ok { + return false + } + return isSelfEqualType(nav.Type()) +} + +// isSelfEqualType indicates whether all runtime values of the given type are equal to themselves. +func isSelfEqualType(t *types.Type) bool { + if t == nil { + return false + } + switch t.Kind() { + case types.BoolKind, types.BytesKind, types.DurationKind, types.IntKind, + types.NullTypeKind, types.StringKind, types.TimestampKind, types.TypeKind, + types.UintKind: + return true + case types.ListKind, types.MapKind: + // Aggregates compare element-wise, so they are self-equal exactly when their type + // parameters are. A list(dyn) or map(string, double) may still contain a NaN. + for _, p := range t.Parameters() { + if !isSelfEqualType(p) { + return false + } + } + return true + default: + return false + } +} + +func isExprConstantOfKind(e ast.Expr, t *types.Type) bool { + return e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == t +} + +func hasComprehensionVar(e ast.NavigableExpr) bool { + idents := ast.MatchDescendants(e, ast.KindMatcher(ast.IdentKind)) + for _, identNode := range idents { + identName := identNode.AsIdent() + curr := identNode + parent, found := curr.Parent() + for found { + if parent.Kind() == ast.ComprehensionKind { + compre := parent.AsComprehension() + if (compre.AccuVar() == identName || compre.IterVar() == identName || compre.IterVar2() == identName) && + curr.ID() != compre.IterRange().ID() && curr.ID() != compre.AccuInit().ID() { + return true + } + } + curr = parent + parent, found = parent.Parent() + } + } + return false +} + func isNestedComprehension(e ast.NavigableExpr) bool { parent, found := e.Parent() for found { diff --git a/vendor/github.com/google/cel-go/cel/inlining.go b/vendor/cel.dev/cel-go/cel/inlining.go similarity index 97% rename from vendor/github.com/google/cel-go/cel/inlining.go rename to vendor/cel.dev/cel-go/cel/inlining.go index d9a5e89a5f5..3ade2204d0c 100644 --- a/vendor/github.com/google/cel-go/cel/inlining.go +++ b/vendor/cel.dev/cel-go/cel/inlining.go @@ -15,12 +15,12 @@ package cel import ( - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/traits" ) // InlineVariable holds a variable name to be matched and an AST representing diff --git a/vendor/github.com/google/cel-go/cel/io.go b/vendor/cel.dev/cel-go/cel/io.go similarity index 81% rename from vendor/github.com/google/cel-go/cel/io.go rename to vendor/cel.dev/cel-go/cel/io.go index 2e611228d95..3af85c59a64 100644 --- a/vendor/github.com/google/cel-go/cel/io.go +++ b/vendor/cel.dev/cel-go/cel/io.go @@ -21,12 +21,12 @@ import ( "google.golang.org/protobuf/proto" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/parser" celpb "cel.dev/expr" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" @@ -52,7 +52,12 @@ func CheckedExprToAstWithSource(checkedExpr *exprpb.CheckedExpr, src Source) (*A if err != nil { return nil, err } - return &Ast{source: src, impl: checked}, nil + out := &Ast{source: src, impl: checked} + if err := checkLoadedASTDepth(checked); err != nil { + out.loadErr = err + return out, err + } + return out, nil } // AstToCheckedExpr converts an Ast to an protobuf CheckedExpr value. @@ -83,7 +88,26 @@ func ParsedExprToAstWithSource(parsedExpr *exprpb.ParsedExpr, src Source) *Ast { src = common.NewInfoSource(parsedExpr.GetSourceInfo()) } e, _ := ast.ProtoToExpr(parsedExpr.GetExpr()) - return &Ast{source: src, impl: ast.NewAST(e, info)} + out := &Ast{source: src, impl: ast.NewAST(e, info)} + // ParsedExprToAstWithSource has no error return, so record an over-depth violation on the Ast + // to be surfaced when it is later checked or planned. + out.loadErr = checkLoadedASTDepth(out.impl) + return out +} + +// checkLoadedASTDepth guards ASTs that enter through the proto conversion helpers +// (ParsedExprToAst / CheckedExprToAst) against nesting deeper than the parser's recursion limit. +// Those entry points bypass the parser, so without this check a deeply nested loaded AST could +// exhaust the Go stack during later checking or planning. It returns a normal error rather than +// risking that overflow; the traversal itself is bounded so it stays safe on the same input. +// +// Embedders that fully control their AST inputs can skip this by building the AST through the +// common/ast package directly instead of these conversion helpers. +func checkLoadedASTDepth(a *ast.AST) error { + if ast.ExceedsDepth(a, defaultMaxASTDepth) { + return fmt.Errorf("input exceeds maximum expression nesting depth: %d", defaultMaxASTDepth) + } + return nil } // AstToParsedExpr converts an Ast to an protobuf ParsedExpr value. @@ -94,18 +118,42 @@ func AstToParsedExpr(a *Ast) (*exprpb.ParsedExpr, error) { }, nil } +// ASTFormatOption is a functional option for configuring the output formatting of AstToString and ExprToString. +type ASTFormatOption func() parser.UnparserOption + +// FormatEscapeIdentifiers configures whether non-standard or reserved identifiers are quoted with backticks. +func FormatEscapeIdentifiers(escape bool) ASTFormatOption { + return func() parser.UnparserOption { + return parser.EscapeIdentifiers(escape) + } +} + +// FormatWrapOnColumn configures expression word-wrapping when the string length exceeds the specified column limit. +func FormatWrapOnColumn(col int) ASTFormatOption { + return func() parser.UnparserOption { + return parser.WrapOnColumn(col) + } +} + // AstToString converts an Ast back to a string if possible. // // Note, the conversion may not be an exact replica of the original expression, but will produce // a string that is semantically equivalent and whose textual representation is stable. -func AstToString(a *Ast) (string, error) { - return ExprToString(a.NativeRep().Expr(), a.NativeRep().SourceInfo()) +func AstToString(a *Ast, opts ...ASTFormatOption) (string, error) { + if a == nil { + return "", errors.New("unsupported expr") + } + return ExprToString(a.NativeRep().Expr(), a.NativeRep().SourceInfo(), opts...) } // ExprToString converts an AST Expr node back to a string using macro call tracking metadata from // source info if any macros are encountered within the expression. -func ExprToString(e ast.Expr, info *ast.SourceInfo) (string, error) { - return parser.Unparse(e, info) +func ExprToString(e ast.Expr, info *ast.SourceInfo, opts ...ASTFormatOption) (string, error) { + unparserOpts := make([]parser.UnparserOption, len(opts)) + for i, opt := range opts { + unparserOpts[i] = opt() + } + return parser.Unparse(e, info, unparserOpts...) } // RefValueToValue converts between ref.Val and google.api.expr.v1alpha1.Value. diff --git a/vendor/github.com/google/cel-go/cel/library.go b/vendor/cel.dev/cel-go/cel/library.go similarity index 73% rename from vendor/github.com/google/cel-go/cel/library.go rename to vendor/cel.dev/cel-go/cel/library.go index 332eb3f1703..933551228a4 100644 --- a/vendor/github.com/google/cel-go/cel/library.go +++ b/vendor/cel.dev/cel-go/cel/library.go @@ -18,18 +18,19 @@ import ( "fmt" "math" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/env" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/stdlib" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/env" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/stdlib" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/interpreter" + "cel.dev/cel-go/parser" ) const ( @@ -42,7 +43,11 @@ const ( optionalOfNonZeroValueFunc = "optional.ofNonZeroValue" optionalUnwrapFunc = "optional.unwrap" valueFunc = "value" - unusedIterVar = "#unused" + + optionalHasValueValueOverload = "optional_hasValue_value" + + unusedIterVar = "#unused" + targetVar = "@target" ) // Library provides a collection of EnvOption and ProgramOption values used to configure a CEL @@ -88,6 +93,11 @@ type LibraryVersioner interface { LibraryVersion() uint32 } +// LibraryCataloguer provides catalog symbols exported by the library when it is not enabled. +type LibraryCataloguer interface { + CatalogSymbols() []*env.CatalogSymbol +} + // Lib creates an EnvOption out of a Library, allowing libraries to be provided as functional args, // and to be linked to each other. func Lib(l Library) EnvOption { @@ -97,6 +107,7 @@ func Lib(l Library) EnvOption { if e.HasLibrary(singleton.LibraryName()) { return e, nil } + e.ensureMutableLibraries() e.libraries[singleton.LibraryName()] = singleton } var err error @@ -182,6 +193,9 @@ func (lib *stdLibrary) CompileOptions() []EnvOption { if err = lib.subset.Validate(); err != nil { return nil, err } + if len(funcs) > 0 { + e.ensureMutableFunctions() + } for _, fn := range funcs { existing, found := e.functions[fn.Name()] if found { @@ -310,24 +324,38 @@ func (*stdLibrary) ProgramOptions() []ProgramOption { // // # OptMap // -// Apply a transformation to the optional's underlying value if it is not empty -// and return an optional typed result based on the transformation. The -// transformation expression type must return a type T which is wrapped into -// an optional. +// Applies a transformation to the optional's value if it holds one, and wraps the +// result in an optional. An optional which holds no value is returned unchanged. +// Starting in **OptionalTypes library version 4**, `optMap` avoids evaluating +// complex target expressions more than once. The change for more efficient +// evaluation alters the cost model in a backward incompatible manner, hence +// the versioned rollout. // -// msg.?elements.optMap(e, e.size()).orValue(0) +// .optMap(, ) -> +// +// Examples: +// +// optional.of(2).optMap(i, i * 2).value() == 4 +// optional.none().optMap(i, i * 2) == optional.none() // // # OptFlatMap // // Introduced in version: 1 // -// Apply a transformation to the optional's underlying value if it is not empty -// and return the result. The transform expression must return an optional(T) -// rather than type T. This can be useful when dealing with zero values and -// conditionally generating an empty or non-empty result in ways which cannot -// be expressed with `optMap`. +// Applies a transformation to the optional's value if it holds one, and returns +// the result. Unlike `optMap`, the transformation must itself produce an +// optional, which makes it possible to express results that may be empty in ways +// `optMap` cannot. Starting in **OptionalTypes library version 4**, `optFlatMap` +// avoids evaluating complex target expressions more than once. The change for more +// efficient evaluation alters the cost model in a backward incompatible manner, +// hence the versioned rollout. // -// msg.?elements.optFlatMap(e, e[?0]) // return the first element if present. +// .optFlatMap(, ) -> +// +// Examples: +// +// {'key': {'sub': 'value'}}.?key.optFlatMap(k, k.?sub).value() == 'value' +// {'key': {}}.?key.optFlatMap(k, k.?sub) == optional.none() // // # First // @@ -336,7 +364,7 @@ func (*stdLibrary) ProgramOptions() []ProgramOption { // Returns an optional with the first value from the right hand list, or // optional.None. // -// [1, 2, 3].first().value() == 1 +// [1, 2, 3].first().value() == 1 // // # Last // @@ -345,7 +373,7 @@ func (*stdLibrary) ProgramOptions() []ProgramOption { // Returns an optional with the last value from the right hand list, or // optional.None. // -// [1, 2, 3].last().value() == 3 +// [1, 2, 3].last().value() == 3 // // This is syntactic sugar for msg.elements[msg.elements.size()-1]. // @@ -358,6 +386,27 @@ func (*stdLibrary) ProgramOptions() []ProgramOption { // // optional.unwrap([optional.of(42), optional.none()]) == [42] // [optional.of(42), optional.none()].unwrapOpt() == [42] +// +// # HasValue with a value argument +// +// Introduced in version: 3 +// +// Determine whether the optional contains a value equal to the argument, which +// is equivalent to the expression `opt.hasValue() ? opt.value() == v : false`. +// +// optional.of(42).hasValue(42) // true +// optional.of(42).hasValue(21) // false +// optional.none().hasValue(42) // false +// +// The argument is compared against the value held by the optional and is never +// unwrapped, so an optional_type(T) argument is only equal to the value of an +// optional_type(optional_type(T)). The type-checker enforces this agreement, +// but when the argument is dyn-typed the comparison is deferred to runtime +// where a type mismatch evaluates to false, just as it would for `==`. +// +// optional.of(optional.of(42)).hasValue(optional.of(42)) // true +// optional.of(42).hasValue(dyn(optional.of(42))) // false +// optional.of(42).hasValue(dyn(42)) // true func OptionalTypes(opts ...OptionalTypesOption) EnvOption { lib := &optionalLib{version: math.MaxUint32} for _, opt := range opts { @@ -404,6 +453,60 @@ func (lib *optionalLib) LibraryVersion() uint32 { return lib.version } +// CatalogSymbols implements the LibraryCataloguer interface method. +func (*optionalLib) CatalogSymbols() []*env.CatalogSymbol { + return []*env.CatalogSymbol{ + { + Name: "optional", + Kind: env.NamespaceKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: "optional_type", + Kind: env.TypeKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: optionalNoneFunc, + Kind: env.FunctionKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: optionalOfFunc, + Kind: env.FunctionKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: optionalOfNonZeroValueFunc, + Kind: env.FunctionKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: optMapMacro, + Kind: env.MacroKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: optFlatMapMacro, + Kind: env.MacroKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + { + Name: "optSelect", + Kind: env.MacroKind, + Library: "cel.lib.optional", + Option: "cel.OptionalTypes()", + }, + } +} + // CompileOptions implements the Library interface method. func (lib *optionalLib) CompileOptions() []EnvOption { paramTypeK := TypeParamType("K") @@ -413,6 +516,11 @@ func (lib *optionalLib) CompileOptions() []EnvOption { mapTypeKV := MapType(paramTypeK, paramTypeV) listOptionalTypeV := ListType(optionalTypeV) + optMapMacroExpander := optMap + if lib.version >= 4 { + optMapMacroExpander = optMapV2 + } + opts := []EnvOption{ // Enable the optional syntax in the parser. enableOptionalSyntax(), @@ -421,7 +529,7 @@ func (lib *optionalLib) CompileOptions() []EnvOption { Types(types.OptionalType), // Configure the optMap and optFlatMap macros. - Macros(ReceiverMacro(optMapMacro, 2, optMap, + Macros(ReceiverMacro(optMapMacro, 2, optMapMacroExpander, MacroDocs(`perform computation on the value if present and return the result as an optional`), MacroExamples( common.MultilineDescription( @@ -453,7 +561,7 @@ func (lib *optionalLib) CompileOptions() []EnvOption { }))), Function(optionalNoneFunc, FunctionDocs(`singleton value representing an optional without a value`), - Overload("optional_none", []*Type{}, optionalTypeV, + Overload(overloads.OptionalNone, []*Type{}, optionalTypeV, OverloadExamples(`optional.none()`), FunctionBinding(func(values ...ref.Val) ref.Val { return types.OptionalNone @@ -525,7 +633,11 @@ func (lib *optionalLib) CompileOptions() []EnvOption { Overload("optional_map_index_value", []*Type{OptionalType(mapTypeKV), paramTypeK}, optionalTypeV)), } if lib.version >= 1 { - opts = append(opts, Macros(ReceiverMacro(optFlatMapMacro, 2, optFlatMap, + optFlatMapMacroExpander := optFlatMap + if lib.version >= 4 { + optFlatMapMacroExpander = optFlatMapV2 + } + opts = append(opts, Macros(ReceiverMacro(optFlatMapMacro, 2, optFlatMapMacroExpander, MacroDocs(`perform computation on the value if present and produce an optional value within the computation`), MacroExamples( common.MultilineDescription( @@ -584,14 +696,108 @@ func (lib *optionalLib) CompileOptions() []EnvOption { UnaryBinding(optUnwrap)))) } + if lib.version >= 3 { + opts = append(opts, Function(hasValueFunc, + MemberOverload(optionalHasValueValueOverload, []*Type{optionalTypeV, paramTypeV}, BoolType, + OverloadExamples( + `optional.of(1).hasValue(1) // true`, + `optional.of(1).hasValue(2) // false`, + `optional.none().hasValue(1) // false`, + common.MultilineDescription( + `// the argument is compared against the optional's value, and is not unwrapped`, + `optional.of(1).hasValue(dyn(optional.of(1))) // false`, + `optional.of(optional.of(1)).hasValue(optional.of(1)) // true`)), + BinaryBinding(func(value, other ref.Val) ref.Val { + opt := value.(*types.Optional) + if !opt.HasValue() { + return types.False + } + return types.Equal(opt.GetValue(), other) + })))) + opts = append(opts, CostEstimatorOptions( + cost.OverloadCostEstimate(optionalHasValueValueOverload, estimateOptionalHasValue))) + } + return opts } // ProgramOptions implements the Library interface method. func (lib *optionalLib) ProgramOptions() []ProgramOption { - return []ProgramOption{ + opts := []ProgramOption{ CustomDecoratorV2(decorateOptionalOr), } + if lib.version >= 3 { + opts = append(opts, CostTrackerOptions( + cost.OverloadTracker(optionalHasValueValueOverload, trackOptionalHasValue))) + } + return opts +} + +// estimateOptionalHasValue estimates the cost of comparing an optional's value against the input +// argument. +// +// The overload is sugar for 'opt.hasValue() ? opt.value() == v : false' where the equality check +// dominates the cost, so the estimate mirrors the O(min(m, n)) cost of an equality comparison. +func estimateOptionalHasValue(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if target == nil || len(args) != 1 { + return nil + } + optSize := sizeOrUnknown(*target) + argSize := sizeOrUnknown(args[0]) + smallestMax := optSize.Max + if argSize.Max < smallestMax { + smallestMax = argSize.Max + } + minCost := uint64(0) + if smallestMax > 0 { + // equality of 2 scalar values results in a cost of 1 + minCost = 1 + } + return &cost.CallEstimate{ + CostEstimate: cost.CostEstimate{Min: minCost, Max: smallestMax}. + MultiplyByCostFactor(cost.StringTraversalCostFactor), + } +} + +// sizeOrUnknown returns the size estimate computed for the node, or an unknown size if the node +// size could not be determined at check time. +func sizeOrUnknown(node cost.AstNode) cost.SizeEstimate { + if sz := node.ComputedSize(); sz != nil { + return *sz + } + return cost.UnknownSizeEstimate() +} + +// trackOptionalHasValue computes the runtime cost of comparing an optional's value against the +// input argument, which is bounded by the size of the smaller of the two values. +func trackOptionalHasValue(args []ref.Val, result ref.Val) *uint64 { + if len(args) != 2 { + return nil + } + callCost := cost.SafeMultiplyByFactor( + min(optionalValueSize(args[0]), optionalValueSize(args[1])), cost.StringTraversalCostFactor) + return &callCost +} + +// optionalValueSize returns the size of the value being compared, unwrapping nested optional +// values to the value they contain, and reporting a size of 1 for values which are not sizable. +func optionalValueSize(value ref.Val) uint64 { + for { + opt, isOpt := value.(*types.Optional) + if !isOpt { + break + } + if !opt.HasValue() { + return 1 + } + value = opt.GetValue() + } + if sizer, isSizer := value.(traits.Sizer); isSizer { + if sz, isInt := sizer.Size().(types.Int); isInt { + return uint64(sz) + } + } + return 1 } // Version returns the current version of the library. @@ -627,6 +833,50 @@ func optMap(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, * ), nil } +func optMapV2(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *Error) { + varIdent := args[0] + varName := "" + switch varIdent.Kind() { + case ast.IdentKind: + varName = varIdent.AsIdent() + default: + return nil, meh.NewError(varIdent.ID(), "optMap() variable name must be a simple identifier") + } + mapExpr := args[1] + targetIdent := target + if target.Kind() != ast.IdentKind { + targetIdent = meh.NewIdent(targetVar) + } + res := meh.NewCall( + operators.Conditional, + meh.NewMemberCall(hasValueFunc, targetIdent), + meh.NewCall(optionalOfFunc, + meh.NewComprehension( + meh.NewList(), + unusedIterVar, + varName, + meh.NewMemberCall(valueFunc, meh.Copy(targetIdent)), + meh.NewLiteral(types.False), + meh.NewIdent(varName), + mapExpr, + ), + ), + meh.NewCall(optionalNoneFunc), + ) + if target.Kind() != ast.IdentKind { + return meh.NewComprehension( + meh.NewList(), + unusedIterVar, + targetVar, + target, + meh.NewLiteral(types.False), + meh.NewIdent(targetVar), + res, + ), nil + } + return res, nil +} + func optFlatMap(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *Error) { varIdent := args[0] varName := "" @@ -653,6 +903,48 @@ func optFlatMap(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Exp ), nil } +func optFlatMapV2(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *Error) { + varIdent := args[0] + varName := "" + switch varIdent.Kind() { + case ast.IdentKind: + varName = varIdent.AsIdent() + default: + return nil, meh.NewError(varIdent.ID(), "optFlatMap() variable name must be a simple identifier") + } + mapExpr := args[1] + targetIdent := target + if target.Kind() != ast.IdentKind { + targetIdent = meh.NewIdent(targetVar) + } + res := meh.NewCall( + operators.Conditional, + meh.NewMemberCall(hasValueFunc, targetIdent), + meh.NewComprehension( + meh.NewList(), + unusedIterVar, + varName, + meh.NewMemberCall(valueFunc, meh.Copy(targetIdent)), + meh.NewLiteral(types.False), + meh.NewIdent(varName), + mapExpr, + ), + meh.NewCall(optionalNoneFunc), + ) + if target.Kind() != ast.IdentKind { + return meh.NewComprehension( + meh.NewList(), + unusedIterVar, + targetVar, + target, + meh.NewLiteral(types.False), + meh.NewIdent(targetVar), + res, + ), nil + } + return res, nil +} + func optUnwrap(value ref.Val) ref.Val { list := value.(traits.Lister) var unwrappedList []ref.Val @@ -748,7 +1040,7 @@ func (opt *evalOptionalOr) Exec(frame *interpreter.ExecutionFrame) ref.Val { // 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)) + return interpreter.EvalActivation(ctx, opt.Exec) } // evalOptionalOrValue selects between an optional or a concrete value. If the optional has a value, @@ -784,7 +1076,7 @@ func (opt *evalOptionalOrValue) Exec(frame *interpreter.ExecutionFrame) ref.Val // 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)) + return interpreter.EvalActivation(ctx, opt.Exec) } type timeLegacyLibrary struct{} diff --git a/vendor/github.com/google/cel-go/cel/macro.go b/vendor/cel.dev/cel-go/cel/macro.go similarity index 99% rename from vendor/github.com/google/cel-go/cel/macro.go rename to vendor/cel.dev/cel-go/cel/macro.go index 3d3c5be1b7c..aed4a8da1fe 100644 --- a/vendor/github.com/google/cel-go/cel/macro.go +++ b/vendor/cel.dev/cel-go/cel/macro.go @@ -17,10 +17,10 @@ package cel import ( "fmt" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/parser" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) diff --git a/vendor/github.com/google/cel-go/cel/optimizer.go b/vendor/cel.dev/cel-go/cel/optimizer.go similarity index 98% rename from vendor/github.com/google/cel-go/cel/optimizer.go rename to vendor/cel.dev/cel-go/cel/optimizer.go index 6e260a93cf8..97dba5f44a8 100644 --- a/vendor/github.com/google/cel-go/cel/optimizer.go +++ b/vendor/cel.dev/cel-go/cel/optimizer.go @@ -18,10 +18,10 @@ import ( "fmt" "sort" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // StaticOptimizer contains a sequence of ASTOptimizer instances which will be applied in order. @@ -38,6 +38,7 @@ type StaticOptimizer struct { sourceOverride *Source } +// OptimizerOption configures a StaticOptimizer instance. type OptimizerOption func(*StaticOptimizer) (*StaticOptimizer, error) // NewStaticOptimizer creates a StaticOptimizer with a sequence of ASTOptimizer's to be applied @@ -317,6 +318,11 @@ func (opt *optimizerExprFactory) MacroCalls() map[int64]ast.Expr { return opt.sourceInfo.MacroCalls() } +// AddExtension adds an extension record into the tracked source info metadata. +func (opt *optimizerExprFactory) AddExtension(ext ast.Extension) { + opt.sourceInfo.AddExtension(ext) +} + // NewBindMacro creates an AST expression representing the expanded bind() macro, and a macro expression // representing the unexpanded call signature to be inserted into the source info macro call metadata. func (opt *optimizerExprFactory) NewBindMacro(macroID int64, varName string, varInit, remaining ast.Expr) (astExpr, macroExpr ast.Expr) { diff --git a/vendor/github.com/google/cel-go/cel/options.go b/vendor/cel.dev/cel-go/cel/options.go similarity index 75% rename from vendor/github.com/google/cel-go/cel/options.go rename to vendor/cel.dev/cel-go/cel/options.go index 86a98d35dba..9ec3e2d8b46 100644 --- a/vendor/github.com/google/cel-go/cel/options.go +++ b/vendor/cel.dev/cel-go/cel/options.go @@ -17,6 +17,7 @@ package cel import ( "errors" "fmt" + "reflect" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" @@ -24,16 +25,17 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/env" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/pb" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/cel/async" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/env" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/pb" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" + "cel.dev/cel-go/parser" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" descpb "google.golang.org/protobuf/types/descriptorpb" @@ -109,12 +111,26 @@ const ( limitCodePointSize // The number of attempts to recover from a parse error. limitParseErrorRecovery + // The maximum nesting depth permitted for ASTs loaded outside the parser. + limitMaxASTDepth + // The maximum number of expression nodes permitted in parsing (including macro expansion). + limitExpressionNodeCount + // The maximum regex program plan size permitted. + limitRegexProgramSize ) +// defaultMaxASTDepth mirrors the parser's default maxRecursionDepth (250) and +// is applied to ASTs that enter through non-parser ingestion paths (e.g. via +// ParsedExprToAst / CheckedExprToAst) when no explicit limit is configured. +const defaultMaxASTDepth = 250 + var limitIDsToNames = map[limitID]string{ limitCodePointSize: "cel.limit.expression_code_points", limitParseErrorRecovery: "cel.limit.parse_error_recovery", limitParseRecursionDepth: "cel.limit.parse_recursion_depth", + limitMaxASTDepth: "cel.limit.max_ast_depth", + limitExpressionNodeCount: "cel.limit.expression_node_count", + limitRegexProgramSize: "cel.limit.regex_program_size", } func limitNameByID(id limitID) (string, bool) { @@ -313,6 +329,22 @@ func Abbrevs(qualifiedNames ...string) EnvOption { } } +// NativeTypeDesc describes a native Go struct type for registration with CEL. +type NativeTypeDesc = types.NativeTypeDesc + +// NativeTypeOption is a functional option for configuring handling of native types. +type NativeTypeOption = types.NativeTypeOption + +var ( + // NativeTypeAlias configures a custom CEL type name (alias) for a native type descriptor. + NativeTypeAlias = types.NativeTypeAlias +) + +// NativeTypeFor constructs a NativeTypeDesc for a Go struct type T with the given options. +func NativeTypeFor[T any](opts ...NativeTypeOption) *NativeTypeDesc { + return types.NativeTypeFor[T](opts...) +} + // protoTypeRegistry is an internal-only interface containing the minimum methods required to support // custom types. It is a subset of methods from ref.TypeRegistry. type protoTypeRegistry interface { @@ -320,11 +352,16 @@ type protoTypeRegistry interface { RegisterType(...ref.Type) error } +type nativeTypeRegistry interface { + RegisterNativeType(reflect.Type, ...types.NativeTypeOption) error + RegisterNativeTypeDesc(*types.NativeTypeDesc) error +} + // Types adds one or more type declarations to the environment, allowing for construction of // type-literals whose definitions are included in the common expression built-in set. // -// The input types may either be instances of `proto.Message` or `ref.Type`. Any other type -// provided to this option will result in an error. +// The input types may either be instances of `proto.Message`, `ref.Type`, `reflect.Type`, or `*NativeTypeDesc`. +// Any other type provided to this option will result in an error. // // Well-known protobuf types within the `google.protobuf.*` package are included in the standard // environment by default. @@ -351,6 +388,24 @@ func Types(addTypes ...any) EnvOption { if err != nil { return nil, err } + case reflect.Type: + nreg, isNReg := e.provider.(nativeTypeRegistry) + if !isNReg { + return nil, fmt.Errorf("native types not supported by provider: %T", e.provider) + } + err := nreg.RegisterNativeType(v) + if err != nil { + return nil, err + } + case *types.NativeTypeDesc: + nreg, isNReg := e.provider.(nativeTypeRegistry) + if !isNReg { + return nil, fmt.Errorf("native types not supported by provider: %T", e.provider) + } + err := nreg.RegisterNativeTypeDesc(v) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported type: %T", t) } @@ -482,6 +537,11 @@ func Functions(funcs ...*functions.Overload) ProgramOption { // a Lib EnvOption, vars may shadow variables provided by previously added libraries. // // The vars value may either be an `cel.Activation` instance or a `map[string]any`. +// +// The vars value may supply late-bound function implementations, e.g. via FunctionVars, in which +// case they apply to every evaluation of the program. Only one source of function bindings is +// permitted, so an error is returned when Globals is applied more than once and more than one of +// the activations supplies them. func Globals(vars any) ProgramOption { return func(p *prog) (*prog, error) { defaultVars, err := NewActivation(vars) @@ -491,6 +551,9 @@ func Globals(vars any) ProgramOption { if p.defaultVars != nil { defaultVars = interpreter.NewHierarchicalActivation(p.defaultVars, defaultVars) } + if _, err := interpreter.FindFunctionActivation(defaultVars); err != nil { + return nil, err + } p.defaultVars = defaultVars return p, nil } @@ -638,7 +701,7 @@ func configToEnvOptions(config *env.Config, provider types.Provider, optFactorie // Configure validators for _, val := range config.Validators { - if fac, found := astValidatorFactories[val.Name]; found { + if fac, found := astValidatorFactories()[val.Name]; found { envOpts = append(envOpts, func(e *Env) (*Env, error) { validator, err := fac(val) if err != nil { @@ -714,6 +777,10 @@ const ( // // Deprecated: use ext.StringsValidateFormatCalls() as this option is now a no-op. OptCheckStringFormat EvalOption = 1 << iota + + // OptTrackMemory enables the runtime peak memory tracking and returns the peak watermark within + // evalDetails via func PeakMemory() + OptTrackMemory EvalOption = 1 << iota ) // EvalOptions sets one or more evaluation options which may affect the evaluation or Result. @@ -735,18 +802,114 @@ func InterruptCheckFrequency(checkFrequency uint) ProgramOption { } } +// AsyncCallObserver sets the observer for monitoring asynchronous function calls during ConcurrentEval. +func AsyncCallObserver(observer async.Observer) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncObserver = observer + return p, nil + } +} + +// AsyncCompletionBufferSize sets the size of the buffer for the async completion channel. +// By default, the channel is unbuffered. +func AsyncCompletionBufferSize(size int) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncCompletionBufferSize = size + return p, nil + } +} + +// AsyncMaxConcurrency sets the maximum number of concurrently launched async calls during +// ConcurrentEval. This bounds the number of in-flight async goroutines, so a wide fan-out (such +// as an async call inside a comprehension over a large list) cannot exhaust memory. +// +// A value of 0 (unset) applies a built-in default bound. A positive value sets an explicit bound. +// A negative value disables the limiter (unbounded launches) and should only be used when +// concurrency is bounded by other means. +func AsyncMaxConcurrency(maxConcurrency int) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncMaxConcurrency = maxConcurrency + return p, nil + } +} + +// ConcurrentDrainStrategy configures the strategy for when to re-evaluate the program +// during a ConcurrentEval call after receiving asynchronous completion signals. +// By default, the program re-evaluates immediately after every completion. +func ConcurrentDrainStrategy(strategy async.DrainStrategy) ProgramOption { + return func(p *prog) (*prog, error) { + p.drainStrategy = strategy + return p, nil + } +} + +// BypassAsyncProtection allows for the bypass of hasAsync protection in Eval and ContextEval if present. +// +// By default, expressions in an environment with asynchronous functions will reject calls to +// Eval and ContextEval, requiring ConcurrentEval instead. This option bypasses that check, allowing +// synchronous evaluation of expressions that do not actually call asynchronous functions (such as +// constant validations). +func BypassAsyncProtection() ProgramOption { + return func(p *prog) (*prog, error) { + p.hasAsync = false + return p, nil + } +} + // CostEstimatorOptions configure type-check time options for estimating expression cost. -func CostEstimatorOptions(costOpts ...checker.CostOption) EnvOption { +func CostEstimatorOptions(costOpts ...cost.CostOption) EnvOption { return func(e *Env) (*Env, error) { e.costOptions = append(e.costOptions, costOpts...) return e, nil } } +// CostModel configures overload cost estimators and cost trackers from a set of cost.OverloadModel definitions. +// +// Models are validated here because neither the estimator nor the tracker compiled from a model +// can report a problem once evaluation has begun. See cost.OverloadModel.Validate. +func CostModel(models ...cost.OverloadModel) EnvOption { + return func(e *Env) (*Env, error) { + if err := cost.ValidateOverloadModels(models...); err != nil { + return nil, err + } + e.costConfig.models = append(e.costConfig.models, models...) + return e, nil + } +} + +// CostSizingStrategy configures a SizingStrategy for both cost estimation and runtime cost tracking. +func CostSizingStrategy(strategy cost.SizingStrategy) EnvOption { + return func(e *Env) (*Env, error) { + e.costConfig.strategy = strategy + return e, nil + } +} + +// CostModelVersion pins cost estimation and runtime cost tracking to a revision of the cost +// model's rules. +// +// The pin applies to the standard models and to any model supplied via CostModel, for both +// estimation and tracking, so an environment's estimates and charges come from one consistent set +// of rules. +// +// If this option is not set, the latest cost model version is used. Use this only as a temporary +// migration aid to hold estimates stable against a baseline recorded under an earlier release. +// Because version 0 preserves legacy behaviors with known soundness gaps where static estimates can +// underestimate runtime cost, callers are strongly encouraged to switch to CostModelVersion(1) or +// higher (or omit this option to use the latest revision) for improved soundness; see +// cost.ModelVersion for the per-revision details. +func CostModelVersion(version uint32) EnvOption { + return func(e *Env) (*Env, error) { + e.costConfig.version = &version + return e, nil + } +} + // CostTrackerOptions configures a set of options for cost-tracking. // // Note, CostTrackerOptions is a no-op unless CostTracking is also enabled. -func CostTrackerOptions(costOpts ...interpreter.CostTrackerOption) ProgramOption { +func CostTrackerOptions(costOpts ...cost.TrackerOption) ProgramOption { return func(p *prog) (*prog, error) { p.costOptions = append(p.costOptions, costOpts...) return p, nil @@ -754,7 +917,7 @@ func CostTrackerOptions(costOpts ...interpreter.CostTrackerOption) ProgramOption } // CostTracking enables cost tracking and registers a ActualCostEstimator that can optionally provide a runtime cost estimate for any function calls. -func CostTracking(costEstimator interpreter.ActualCostEstimator) ProgramOption { +func CostTracking(costEstimator cost.ActualCostEstimator) ProgramOption { return func(p *prog) (*prog, error) { p.callCostEstimator = costEstimator p.evalOpts |= OptTrackCost @@ -762,6 +925,36 @@ func CostTracking(costEstimator interpreter.ActualCostEstimator) ProgramOption { } } +// MemoryTracking enables peak memory tracking during evaluation with an optional set of +// types.MemoryTrackerOption values to configure the tracker's size calculator, sample +// interval, and limit behaviors. +// +// Peak memory is measured in aggregate element counts as computed by a types.SizeCalculator +// and is observed at the points where values materialize during evaluation: resolved +// attributes, call results, constructed aggregates, and comprehension results. The peak +// watermark is available via the EvalDetails.PeakMemory() method. +func MemoryTracking(memOpts ...types.MemoryTrackerOption) ProgramOption { + return func(p *prog) (*prog, error) { + p.memoryOptions = append(p.memoryOptions, memOpts...) + p.evalOpts |= OptTrackMemory + return p, nil + } +} + +// MemoryLimit enables memory tracking and configures program evaluation to exit early with a +// "memory limit exceeded" error if the peak tracked memory exceeds the limit. +// +// The MemoryLimit is a metric that corresponds to the aggregate element counts of the values +// observed during evaluation. It is indicative of memory usage, not CPU usage; see CostLimit +// for bounding compute. +func MemoryLimit(memLimit uint32) ProgramOption { + return func(p *prog) (*prog, error) { + p.memoryLimit = &memLimit + p.evalOpts |= OptTrackMemory + return p, nil + } +} + // CostLimit enables cost tracking and sets configures program evaluation to exit early with a // "runtime cost limit exceeded" error if the runtime cost exceeds the costLimit. // The CostLimit is a metric that corresponds to the number and estimated expense of operations @@ -864,7 +1057,11 @@ func ContextProtoVars(ctx proto.Message, opts ...types.RegistryOption) (Activati } regOpts := []types.RegistryOption{types.ProtoTypeDefs(ctx)} regOpts = append(regOpts, opts...) - reg, err := types.NewProtoRegistry(regOpts...) + var ro []any + for _, opt := range regOpts { + ro = append(ro, opt) + } + reg, err := types.NewRegistry(ro...) if err != nil { return nil, err } @@ -893,6 +1090,11 @@ func ContextProtoVars(ctx proto.Message, opts ...types.RegistryOption) (Activati // EnableMacroCallTracking ensures that call expressions which are replaced by macros // are tracked in the `SourceInfo` of parsed and checked expressions. +// +// This is required for Env.ResidualAst to succeed when the residual Ast contains an +// unresolved comprehension macro (all, exists, exists_one, filter, map): without it, +// ResidualAst cannot re-serialize the macro's expanded internal representation back +// into CEL source syntax and returns an error instead of the residual expression. func EnableMacroCallTracking() EnvOption { return features(featureEnableMacroCallTracking, true) } @@ -917,6 +1119,7 @@ func DefaultUTCTimeZone(enabled bool) EnvOption { // features sets the given feature flags. See list of Feature constants above. func features(flag int, enabled bool) EnvOption { return func(e *Env) (*Env, error) { + e.ensureMutableFeatures() e.features[flag] = enabled return e, nil } @@ -927,6 +1130,7 @@ func setLimit(id limitID, limit int) EnvOption { limit = -1 } return func(e *Env) (*Env, error) { + e.ensureMutableLimits() e.limits[id] = limit return e, nil } @@ -950,6 +1154,44 @@ func ParserExpressionSizeLimit(limit int) EnvOption { return setLimit(limitCodePointSize, limit) } +// ExpressionNodeLimit adjusts the maximum number of expression nodes permitted during parsing +// and checking, including nodes created by macro expansion. Defaults are defined in the parser +// package (100,000). A negative value means unbounded. +func ExpressionNodeLimit(limit int) EnvOption { + return setLimit(limitExpressionNodeCount, limit) +} + +// ExpressionNestingDepthLimit records the maximum nesting depth permitted for ASTs in the +// environment configuration so that the value round-trips through env.Config export/import. +// +// ASTs loaded outside the parser (e.g. via ParsedExprToAst / CheckedExprToAst) bypass the +// parser's recursion limit, so those conversion paths validate nesting depth against the +// parser-matching default (250) to avoid a Go stack overflow during later checking or planning. +// Embedders that fully control their AST inputs and want to skip the check can construct the AST +// through the common/ast package directly rather than the cel conversion helpers. +func ExpressionNestingDepthLimit(limit int) EnvOption { + return setLimit(limitMaxASTDepth, limit) +} + +// RegexProgramSizeLimit caps the maximum regex program plan size permitted for regular expressions. +// A negative or zero value means unbounded. +func RegexProgramSizeLimit(limit int) EnvOption { + return func(e *Env) (*Env, error) { + var err error + e, err = setLimit(limitRegexProgramSize, limit)(e) + if err != nil { + return nil, err + } + if limit > 0 { + e, err = ASTValidators(ValidateRegexProgramSizeLimit(limit))(e) + if err != nil { + return nil, err + } + } + return e, nil + } +} + // EnableHiddenAccumulatorName sets the parser to use the identifier '@result' for accumulators // which is not normally accessible from CEL source. func EnableHiddenAccumulatorName(enabled bool) EnvOption { @@ -959,11 +1201,19 @@ func EnableHiddenAccumulatorName(enabled bool) EnvOption { } } +type legacyTypeProvider interface { + EnumValue(enumName string) ref.Val + FindIdent(identName string) (ref.Val, bool) + FindType(typeName string) (*exprpb.Type, bool) + FindFieldType(messageType, fieldName string) (*ref.FieldType, bool) + NewValue(typeName string, fields map[string]ref.Val) ref.Val +} + func maybeInteropProvider(provider any) (types.Provider, error) { switch p := provider.(type) { case types.Provider: return p, nil - case ref.TypeProvider: + case legacyTypeProvider: return &interopCELTypeProvider{TypeProvider: p}, nil default: return nil, fmt.Errorf("unsupported type provider: %T", provider) diff --git a/vendor/cel.dev/cel-go/cel/program.go b/vendor/cel.dev/cel-go/cel/program.go new file mode 100644 index 00000000000..60c13b0c31c --- /dev/null +++ b/vendor/cel.dev/cel-go/cel/program.go @@ -0,0 +1,787 @@ +// Copyright 2019 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 cel + +import ( + "context" + "errors" + "fmt" + "time" + + "cel.dev/cel-go/cel/async" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/interpreter" +) + +// Program is an evaluable view of an Ast. +type Program interface { + // Eval returns the result of an evaluation of the Ast and environment against the input vars. + // + // The vars value may either be an `Activation` or a `map[string]any`. + // + // If the `OptTrackState`, `OptTrackCost` or `OptExhaustiveEval` flags are used, the `details` response will + // be non-nil. Given this caveat on `details`, the return state from evaluation will be: + // + // * `val`, `details`, `nil` - Successful evaluation of a non-error result. + // * `val`, `details`, `err` - Successful evaluation to an error result. + // * `nil`, `details`, `err` - Unsuccessful evaluation. + // + // An unsuccessful evaluation is typically the result of a series of incompatible `EnvOption` + // or `ProgramOption` values used in the creation of the evaluation environment or executable + // program. + Eval(any) (ref.Val, *EvalDetails, error) + + // ContextEval evaluates the program with a set of input variables and a context object in order + // to support cancellation and timeouts. This method must be used in conjunction with the + // InterruptCheckFrequency() option for cancellation interrupts to be impact evaluation. + // + // The vars value may either be an `Activation` or `map[string]any`. + // + // The output contract for `ContextEval` is otherwise identical to the `Eval` method. + ContextEval(context.Context, any) (ref.Val, *EvalDetails, error) + + // ConcurrentEval evaluates the program concurrently, returning a channel that will receive + // the final EvalResult when all asynchronous operations complete, or the context expires. + // + // The vars value may either be an `Activation` or `map[string]any`. + // + // Liveness: ConcurrentEval relies on context cancellation to terminate. If an async function + // never returns and does not honor its context, and the supplied context has no deadline, the + // call will block indefinitely. Always pass a context with a deadline or cancellation. + // + // Error handling is fail-fast: as soon as a re-evaluation pass yields an error, that error is + // returned and any still in-flight async calls are cancelled (their contexts are done) and + // their results discarded. Async functions should therefore be free of unwanted side effects + // on partial evaluation, or guard them with idempotency/cancellation handling. + ConcurrentEval(context.Context, any) <-chan EvalResult +} + +// Activation used to resolve identifiers by name and references by id. +// +// An Activation is the primary mechanism by which a caller supplies input into a CEL program. +type Activation = interpreter.Activation + +// NewActivation returns an activation based on a map-based binding where the map keys are +// expected to be qualified names used with ResolveName calls. +// +// The input `bindings` may either be of type `Activation` or `map[string]any`. +// +// Lazy bindings may be supplied within the map-based input in either of the following forms: +// - func() any +// - func() ref.Val +// +// The output of the lazy binding will overwrite the variable reference in the internal map. +// +// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using +// the types.Adapter configured in the environment. +func NewActivation(bindings any) (Activation, error) { + return interpreter.NewActivation(bindings) +} + +// PartialActivation extends the Activation interface with a set of unknown AttributePatterns. +type PartialActivation = interpreter.PartialActivation + +// NoVars returns an empty Activation. +func NoVars() Activation { + return interpreter.EmptyActivation() +} + +// PartialVars returns a PartialActivation which contains variables and a set of AttributePattern +// values that indicate variables or parts of variables whose value are not yet known. +// +// This method relies on manually configured sets of missing attribute patterns. For a method which +// infers the missing variables from the input and the configured environment, use Env.PartialVars(). +// +// The `vars` value may either be an Activation or any valid input to the NewActivation call. +func PartialVars(vars any, + unknowns ...*AttributePatternType) (PartialActivation, error) { + return interpreter.NewPartialActivation(vars, unknowns...) +} + +// FunctionActivation extends the Activation interface with implementations for the functions +// declared with a late binding. +type FunctionActivation = interpreter.FunctionActivation + +// LateBoundFunction is the signature required of all late-bound function implementations. +// +// The overload id indicates which of the declared overloads matched the call arguments, which +// permits a single implementation to serve all overloads of the function. +// +// An implementation which depends on the state of the evaluation should capture that state in a +// closure, as the activation is not an input to the call. +type LateBoundFunction = functions.LateBoundOp + +// FunctionVars returns a FunctionActivation combining the variable bindings from `vars` with +// the late-bound function implementations from `funcs`. FunctionVars support late-bound function +// definitions where the function behavior depends on the context of the evaluation. This can only +// be accomplished by closing over state that is not otherwise exposed to the CEL author or CEL +// runtime. +// +// - Function bindings occupy a namespace separate from variables, allowing a function and a +// variable to share the same qualified name. +// - The `vars` argument may be an Activation or any valid input to NewActivation (e.g. map[string]any). +// - FunctionVars may not be nested, and only one instance of FunctionVars may be supplied to an +// evaluation. FunctionVars returns an error if `vars` already contains function bindings. +// +// Function implementations can capture evaluation-scoped state via closures without exposing that +// state directly as expression variables: +// +// roles := map[string]string{"tristan": "admin"} +// act, err := cel.FunctionVars(map[string]any{"user": "tristan"}, map[string]cel.LateBoundFunction{ +// "role": func(overloadID string, args ...ref.Val) ref.Val { +// role, found := roles[string(args[0].(types.String))] +// if !found { +// return types.NewErr("no role for user: %s", args[0]) +// } +// return types.String(role) +// }, +// }) +// +// CEL expects that late-bound functions, like regular functions, yield the same output given the same +// input. For example, a function comparing a validity window `valid_until(t)` could be implemented as +// a late-bound function closed over a fixed timestamp value. +func FunctionVars(vars any, funcs map[string]LateBoundFunction) (FunctionActivation, error) { + return interpreter.NewFunctionActivation(vars, funcs) +} + +// AttributePattern returns an AttributePattern that matches a top-level variable. The pattern is +// mutable, and its methods support the specification of one or more qualifier patterns. +// +// For example, the AttributePattern(`a`).QualString(`b`) represents a variable access `a` with a +// string field or index qualification `b`. This pattern will match Attributes `a`, and `a.b`, +// but not `a.c`. +// +// When using a CEL expression within a container, e.g. a package or namespace, the variable name +// in the pattern must match the qualified name produced during the variable namespace resolution. +// For example, when variable `a` is declared within an expression whose container is `ns.app`, the +// fully qualified variable name may be `ns.app.a`, `ns.a`, or `a` per the CEL namespace resolution +// rules. Pick the fully qualified variable name that makes sense within the container as the +// AttributePattern `varName` argument. +func AttributePattern(varName string) *AttributePatternType { + return interpreter.NewAttributePattern(varName) +} + +// AttributePatternType represents a top-level variable with an optional set of qualifier patterns. +// +// See the interpreter.AttributePattern and interpreter.AttributeQualifierPattern for more info +// about how to create and manipulate AttributePattern values. +type AttributePatternType = interpreter.AttributePattern + +// EvalDetails holds additional information observed during the Eval() call. +type EvalDetails struct { + state interpreter.EvalState + costTracker *cost.Tracker + memTracker *types.MemoryTracker +} + +// State of the evaluation, non-nil if the OptTrackState or OptExhaustiveEval is specified +// within EvalOptions. +func (ed *EvalDetails) State() interpreter.EvalState { + if ed == nil { + return interpreter.NewEvalState() + } + return ed.state +} + +// ActualCost returns the tracked cost through the course of execution when `CostTracking` is enabled. +// Otherwise, returns nil if the cost was not enabled. +func (ed *EvalDetails) ActualCost() *uint64 { + if ed == nil || ed.costTracker == nil { + return nil + } + cost := ed.costTracker.ActualCost() + return &cost +} + +// PeakMemory returns the peak memory watermark observed through the course of execution when +// `MemoryTracking` is enabled. Otherwise, returns nil if memory tracking was not enabled. +func (ed *EvalDetails) PeakMemory() *uint32 { + if ed == nil || ed.memTracker == nil { + return nil + } + peak := ed.memTracker.Peak() + return &peak +} + +// EvalResult encapsulates the response from a ConcurrentEval call. +type EvalResult struct { + Val ref.Val + EvalDetails *EvalDetails + Err error +} + +// captureCostTracker records the frame's cost tracker within the evaluation details, allocating +// the details when cost tracking is enabled and no details have been created yet. +// +// The details must be fully populated before they are handed off to the caller, as any mutation +// after the hand-off would race with the caller's reads. +func captureCostTracker(det *EvalDetails, frame *interpreter.ExecutionFrame) *EvalDetails { + tracker := frame.CostTracker() + if tracker == nil { + return det + } + if det == nil { + det = &EvalDetails{} + } + det.costTracker = tracker + return det +} + +// prog is the internal implementation of the Program interface. +type prog struct { + *Env + evalOpts EvalOption + defaultVars Activation + dispatcher interpreter.Dispatcher + interpreter interpreter.Interpreter + interruptCheckFrequency uint + + // Intermediate state used to configure the InterpretableDecorator set provided + // to the initInterpretable call. + plannerOptions []interpreter.PlannerOption + regexOptimizations []*interpreter.RegexOptimization + + // Interpretable configured from an Ast and aggregate decorator set based on program options. + interpretable interpreter.InterpretableV2 + observable *interpreter.ObservableInterpretable + callCostEstimator cost.ActualCostEstimator + costOptions []cost.TrackerOption + costLimit *uint64 + memoryOptions []types.MemoryTrackerOption + memoryLimit *uint32 + + // hasAsync indicates the planned expression contains an asynchronous function call, which can + // only be resolved by ConcurrentEval. + hasAsync bool + + // Async evaluation configuration used by ConcurrentEval. + drainStrategy async.DrainStrategy + asyncObserver async.Observer + asyncCompletionBufferSize int + asyncMaxConcurrency int +} + +// scanOptTargets walks the AST once and reports whether the Optimize() +// decorator (needOpt) and the regex-constant compiler (needRegex) have any +// target node present. The conditions are a superset of what each decorator +// acts on, so a false negative — skipping a decorator that would have +// optimized something — is impossible. +func scanOptTargets(root ast.Expr) (needOpt, needRegex bool) { + ast.PostOrderVisit(root, ast.NewExprVisitor(func(e ast.Expr) { + switch e.Kind() { + case ast.ListKind, ast.MapKind: + needOpt = true // maybeBuildListLiteral / maybeBuildMapLiteral + case ast.CallKind: + switch fn := e.AsCall().FunctionName(); { + case fn == overloads.Matches: + needRegex = true + case fn == operators.In || fn == operators.OldIn: + needOpt = true // maybeOptimizeSetMembership + case overloads.IsTypeConversionFunction(fn): + needOpt = true // maybeOptimizeConstUnary + } + } + })) + return +} + +// newProgram creates a program instance with an environment, an ast, and an optional list of +// ProgramOption values. +// +// If the program cannot be configured the prog will be nil, with a non-nil error response. +func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { + // Build the env's function bindings and shared dispatcher once (pure functions of the + // env). The dispatcher holding the env's function bindings is identical across every + // Program() built from it and read-only during planning — so assemble it once per env + // and layer a thin child over it here for per-program Functions() isolation, rather than + // re-indexing overloads on every Program() call. + sharedDisp, hasAsync, err := e.initDispatcher() + if err != nil { + return nil, err + } + disp := interpreter.ExtendDispatcher(sharedDisp) + + // Ensure the default attribute factory is set after the adapter and provider are + // configured. + p := &prog{ + Env: e, + plannerOptions: []interpreter.PlannerOption{}, + dispatcher: disp, + costOptions: []cost.TrackerOption{}, + drainStrategy: async.DrainReady(100 * time.Microsecond), + hasAsync: hasAsync, + } + + // Configure the program via the ProgramOption values. + for _, opt := range opts { + p, err = opt(p) + if err != nil { + return nil, err + } + } + + // Set the attribute factory after the options have been set. + var attrFactory interpreter.AttributeFactory + attrFactorOpts := []interpreter.AttrFactoryOption{ + interpreter.EnableErrorOnBadPresenceTest(p.HasFeature(featureEnableErrorOnBadPresenceTest)), + } + if a.SourceInfo().HasExtension("json_name", ast.NewExtensionVersion(1, 1)) { + if !e.HasFeature(featureJSONFieldNames) { + return nil, errors.New("the AST extension 'json_name' requires the option cel.JSONFieldNames(true)") + } + } + // Configure the type provider, considering whether the AST indicates whether it supports JSON field names + if p.evalOpts&OptPartialEval == OptPartialEval { + attrFactory = interpreter.NewPartialAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...) + } else { + attrFactory = interpreter.NewAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...) + } + interp := interpreter.NewInterpreter(disp, e.Container, e.provider, e.adapter, attrFactory) + p.interpreter = interp + + // Translate the EvalOption flags into InterpretableDecorator instances. + plannerOptions := make([]interpreter.PlannerOption, len(p.plannerOptions)) + copy(plannerOptions, p.plannerOptions) + + // Enable interrupt checking if there's a non-zero check frequency + if p.interruptCheckFrequency > 0 { + plannerOptions = append(plannerOptions, interpreter.InterruptableEval()) + } + // Enable constant folding first. + if p.evalOpts&OptOptimize == OptOptimize { + // The Optimize() decorator (set-membership, constant list/map literals, + // const type conversions) and the regex-constant compiler each walk + // every planned node. When the AST provably contains no node they can + // act on, adding them is pure overhead — so gate each on a single AST + // scan. The scan condition is a superset of what the decorators touch, + // so a decorator is only skipped when its target node is definitely + // absent and evaluation is never affected (an ungated regex would + // recompile per eval, etc.). + addOptimize, addRegex := scanOptTargets(a.Expr()) + if addOptimize { + plannerOptions = append(plannerOptions, interpreter.Optimize()) + } + if addRegex { + p.regexOptimizations = append(p.regexOptimizations, interpreter.MatchesRegexOptimization) + } + } + // Enable regex compilation of constants immediately after folding constants. + if len(p.regexOptimizations) > 0 { + plannerOptions = append(plannerOptions, interpreter.CompileRegexConstants(p.regexOptimizations...)) + } + if limit := p.limits[limitRegexProgramSize]; limit > 0 { + plannerOptions = append(plannerOptions, interpreter.RegexProgramSizeLimit(limit)) + } + + // Enable exhaustive eval, state tracking, cost tracking, and memory tracking last since they + // require a factory. + if p.evalOpts&(OptExhaustiveEval|OptTrackState|OptTrackCost|OptTrackMemory) != 0 { + var observers []interpreter.PlannerOption + if p.evalOpts&(OptExhaustiveEval|OptTrackState) != 0 { + // EvalStateObserver is required for OptExhaustiveEval. + observers = append(observers, interpreter.EvalStateObserver()) + } + if p.evalOpts&OptTrackCost == OptTrackCost { + trackerOpts := p.costModel.tracker + costOptCount := len(trackerOpts) + len(p.costOptions) + 1 + if p.costLimit != nil { + costOptCount++ + } + costOpts := make([]cost.TrackerOption, 0, costOptCount) + costOpts = append(costOpts, cost.TrackerLimitExceededHandler(func() { + panic(interpreter.EvalCancelledError{ + Cause: interpreter.CostLimitExceeded, + Message: "operation cancelled: actual cost limit exceeded", + }) + })) + costOpts = append(costOpts, trackerOpts...) + costOpts = append(costOpts, p.costOptions...) + if p.costLimit != nil { + costOpts = append(costOpts, cost.TrackerLimit(*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 := cost.NewTracker(p.callCostEstimator, costOpts...) + if err != nil { + return nil, fmt.Errorf("construct cost tracker: %w", err) + } + trackerFactory := func() (*cost.Tracker, error) { + return tracker.Clone() + } + plannerOptions = append(plannerOptions, interpreter.CostObserver(interpreter.CostTrackerFactory(trackerFactory))) + } + if p.evalOpts&OptTrackMemory == OptTrackMemory { + memOptCount := len(p.memoryOptions) + if p.memoryLimit != nil { + memOptCount++ + } + memOpts := make([]types.MemoryTrackerOption, 0, memOptCount) + memOpts = append(memOpts, p.memoryOptions...) + if p.memoryLimit != nil { + memOpts = append(memOpts, types.MemoryTrackerLimit(*p.memoryLimit)) + } + memTrackerFactory := func() (*types.MemoryTracker, error) { + return types.NewMemoryTracker(memOpts...), nil + } + observers = append(observers, interpreter.MemoryObserver(interpreter.MemoryTrackerFactory(memTrackerFactory))) + } + // Enable exhaustive eval over a basic observer since it offers a superset of features. + if p.evalOpts&OptExhaustiveEval == OptExhaustiveEval { + plannerOptions = append(plannerOptions, + append([]interpreter.PlannerOption{interpreter.ExhaustiveEval()}, observers...)...) + } else if len(observers) > 0 { + plannerOptions = append(plannerOptions, observers...) + } + } + return p.initInterpretable(a, plannerOptions) +} + +func (p *prog) initInterpretable(a *ast.AST, plannerOptions []interpreter.PlannerOption) (*prog, error) { + // When the AST has been exprAST it contains metadata that can be used to speed up program execution. + interpretable, err := p.interpreter.NewInterpretable(a, plannerOptions...) + if err != nil { + return nil, err + } + p.interpretable = interpretable + if oi, ok := interpretable.(*interpreter.ObservableInterpretable); ok { + p.observable = oi + } + return p, nil +} + +// Eval implements the Program interface method. +func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) { + if p == nil { + return nil, nil, errors.New("program is nil") + } + // Asynchronous calls cannot be resolved by a single-pass evaluation. Reject before doing any + // work (this also covers ContextEval, which delegates here); ConcurrentEval does not call Eval. + if p.hasAsync { + return nil, nil, errAsyncRequiresConcurrentEval + } + // Build a hierarchical activation if there are default vars set. + var frame *interpreter.ExecutionFrame + var mustClose bool + if f, ok := input.(*interpreter.ExecutionFrame); ok { + frame = f + } else { + frame, err = p.newExecutionFrame(input) + if err != nil { + return nil, nil, err + } + mustClose = true + } + // Configure error recovery and details capture for evaluation. + defer func() { + det = captureCostTracker(det, frame) + if r := recover(); r != nil { + switch t := r.(type) { + case interpreter.EvalCancelledError: + err = t + case cost.LimitExceededError: + err = interpreter.EvalCancelledError{ + Cause: interpreter.CostLimitExceeded, + Message: t.Message, + } + default: + err = fmt.Errorf("internal error: %v", r) + } + } + if mustClose { + frame.Close() + } + }() + + if p.observable != nil { + det = &EvalDetails{} + out = p.observable.ObserveExec(frame, func(observed any) { + switch o := observed.(type) { + case interpreter.EvalState: + det.state = o + case *types.MemoryTracker: + det.memTracker = o + } + }) + } else { + 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 + // RPC signature which allows for multiple errors to be returned, but should be sufficient. + if types.IsError(out) { + err = out.(*types.Err) + } + return +} + +// ContextEval implements the Program interface. +func (p *prog) ContextEval(ctx context.Context, input any) (ref.Val, *EvalDetails, error) { + if p == nil { + return nil, nil, errors.New("program is nil") + } + if ctx == nil { + return nil, nil, errors.New("context can not be nil") + } + frame, err := p.newExecutionFrame(input) + if err != nil { + return nil, nil, err + } + 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, errEval +} + +// 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 + } + if p.defaultVars != nil { + if err := frame.SetDefaultVars(p.defaultVars); err != nil { + frame.Close() + return nil, err + } + } + return frame, nil +} + +// newAsyncFrame creates an ExecutionFrame configured for asynchronous evaluation under the +// given context, wiring the observer and concurrency limit from the program options. +func (p *prog) newAsyncFrame(ctx context.Context, input any) (*interpreter.ExecutionFrame, error) { + frame, err := p.newExecutionFrame(input) + if err != nil { + return nil, err + } + if err := frame.SetContext(ctx, p.interruptCheckFrequency); err != nil { + frame.Close() + return nil, err + } + frame.SetAsyncObserver(p.asyncObserver) + frame.SetAsyncMaxConcurrency(resolveAsyncMaxConcurrency(p.asyncMaxConcurrency)) + return frame, nil +} + +// defaultAsyncMaxConcurrency bounds the number of concurrently launched async calls when the +// program does not configure AsyncMaxConcurrency. It exists so that a wide fan-out (e.g. an async +// call inside a comprehension over a large list) cannot spawn an unbounded number of goroutines. +const defaultAsyncMaxConcurrency = 100 + +// resolveAsyncMaxConcurrency maps the configured concurrency to the effective launch limit: +// - 0 (unset): apply defaultAsyncMaxConcurrency. +// - >0: use the configured value. +// - <0: unlimited (no launch limiter); use only if the caller bounds concurrency another way. +func resolveAsyncMaxConcurrency(configured int) int { + if configured == 0 { + return defaultAsyncMaxConcurrency + } + return configured +} + +// resolveCompletionBufferSize returns the size of the async completion channel. When unset, it +// defaults to the effective launch concurrency so that all in-flight calls can report completion +// without blocking. An unbuffered channel would make a completed call hold its launch slot until +// the evaluator drained it, throttling effective concurrency to the drain rate. +func (p *prog) resolveCompletionBufferSize() int { + if p.asyncCompletionBufferSize > 0 { + return p.asyncCompletionBufferSize + } + limit := resolveAsyncMaxConcurrency(p.asyncMaxConcurrency) + if limit < 0 { + // Unlimited launches: fall back to the default bound for the buffer so it stays finite. + return defaultAsyncMaxConcurrency + } + return limit +} + +// ConcurrentEval implements the Program interface. +func (p *prog) ConcurrentEval(ctx context.Context, input any) <-chan EvalResult { + resCh := make(chan EvalResult, 1) + if p == nil { + resCh <- EvalResult{Err: errors.New("program is nil")} + close(resCh) + return resCh + } + if ctx == nil { + resCh <- EvalResult{Err: errors.New("context can not be nil")} + close(resCh) + return resCh + } + + go func() { + defer close(resCh) + frame, err := p.newAsyncFrame(ctx, input) + if err != nil { + resCh <- EvalResult{Err: err} + return + } + defer frame.Close() + + var det *EvalDetails + // Ensure concurrent eval handles panic / recovery properly. + // + // The details are only captured and published here for the panic case. Every normal + // return path populates the details and sends them on the result channel itself, and + // the receiver may read them as soon as the send completes. Mutating det from this + // defer, which runs after the send, would race with those reads. + defer func() { + r := recover() + if r == nil { + return + } + det = captureCostTracker(det, frame) + switch t := r.(type) { + case interpreter.EvalCancelledError: + resCh <- EvalResult{EvalDetails: det, Err: t} + case cost.LimitExceededError: + resCh <- EvalResult{ + EvalDetails: det, + Err: interpreter.EvalCancelledError{ + Cause: interpreter.CostLimitExceeded, + Message: t.Message, + }, + } + default: + resCh <- EvalResult{EvalDetails: det, Err: fmt.Errorf("internal error: %v", r)} + } + }() + + // Completions are signaled to this channel as async calls finish. The asyncCallState + // fan-in also selects on ctx.Done(), so the sender will not leak if this loop returns early. + completions := make(chan int64, p.resolveCompletionBufferSize()) + frame.SetCompletions(completions) + + for { + var out ref.Val + + if p.observable != nil { + det = &EvalDetails{} + out = p.observable.ObserveExec(frame, func(observed any) { + switch o := observed.(type) { + case interpreter.EvalState: + det.state = o + case *types.MemoryTracker: + det.memTracker = o + } + }) + } else { + out = p.interpretable.Exec(frame) + } + // Capture the cost tracker before the result is published on the channel: once the + // send completes the receiver owns the details and they must not be mutated. + det = captureCostTracker(det, frame) + + // Communicate errors quickly. + if types.IsError(out) { + var err error = out.(*types.Err) + if errors.Is(err, interpreter.InterruptError{}) { + err = fmt.Errorf("%w: %w", err, context.Cause(ctx)) + } + resCh <- EvalResult{Val: out, EvalDetails: det, Err: err} + return + } + + // A concrete (non-unknown) result is final. + unk, isUnknown := out.(*types.Unknown) + if !isUnknown || !unk.HasUnknownFunction() { + resCh <- EvalResult{Val: out, EvalDetails: det, Err: nil} + return + } + + // Post-execution dispatch: launch only the async calls required by the unknown result. + frame.DispatchPendingAsyncCalls(unk.IDs()) + + // The result depends on one or more unresolved async calls. Wait for completions and + // re-evaluate according to the configured drain strategy. + var batch []async.Call + + // Wait for at least one completion (or cancellation). + select { + case id := <-completions: + if call := frame.AsyncCall(id); call != nil { + batch = append(batch, call) + } + case <-ctx.Done(): + resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} + return + } + + // Accumulate completions and consult the strategy. + var timer *time.Timer + reevaluate := false + for !reevaluate { + active := frame.ActiveAsyncCalls() + action := p.drainStrategy.NextAction(batch, active) + if action.Reevaluate { + break + } + + var timeoutCh <-chan time.Time + if action.WaitDuration > 0 { + if timer == nil { + timer = time.NewTimer(action.WaitDuration) + } else { + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(action.WaitDuration) + } + timeoutCh = timer.C + } + + select { + case id := <-completions: + if call := frame.AsyncCall(id); call != nil { + batch = append(batch, call) + } + case <-timeoutCh: + reevaluate = true + case <-ctx.Done(): + if timer != nil { + timer.Stop() + } + resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} + return + } + } + if timer != nil { + timer.Stop() + } + } + }() + + return resCh +} + +// errAsyncRequiresConcurrentEval is returned by the synchronous entry points (Eval, ContextEval) +// when the expression contains asynchronous function calls, which only ConcurrentEval can resolve. +var errAsyncRequiresConcurrentEval = errors.New( + "expression contains asynchronous function calls; use ConcurrentEval") diff --git a/vendor/github.com/google/cel-go/cel/prompt.go b/vendor/cel.dev/cel-go/cel/prompt.go similarity index 97% rename from vendor/github.com/google/cel-go/cel/prompt.go rename to vendor/cel.dev/cel-go/cel/prompt.go index f5993482718..e5e2acde7a1 100644 --- a/vendor/github.com/google/cel-go/cel/prompt.go +++ b/vendor/cel.dev/cel-go/cel/prompt.go @@ -20,10 +20,10 @@ import ( "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" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" ) //go:embed templates/authoring.tmpl diff --git a/vendor/github.com/google/cel-go/cel/templates/authoring.tmpl b/vendor/cel.dev/cel-go/cel/templates/authoring.tmpl similarity index 100% rename from vendor/github.com/google/cel-go/cel/templates/authoring.tmpl rename to vendor/cel.dev/cel-go/cel/templates/authoring.tmpl diff --git a/vendor/github.com/google/cel-go/cel/validator.go b/vendor/cel.dev/cel-go/cel/validator.go similarity index 67% rename from vendor/github.com/google/cel-go/cel/validator.go rename to vendor/cel.dev/cel-go/cel/validator.go index 952f88f41b8..a6835be0f03 100644 --- a/vendor/github.com/google/cel-go/cel/validator.go +++ b/vendor/cel.dev/cel-go/cel/validator.go @@ -18,18 +18,22 @@ import ( "fmt" "reflect" "regexp" + "sync" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/env" - "github.com/google/cel-go/common/overloads" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/env" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" ) const ( - durationValidatorName = "cel.validator.duration" - regexValidatorName = "cel.validator.matches" - timestampValidatorName = "cel.validator.timestamp" - homogeneousValidatorName = "cel.validator.homogeneous_literals" - nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit" + durationValidatorName = "cel.validator.duration" + regexValidatorName = "cel.validator.matches" + timestampValidatorName = "cel.validator.timestamp" + homogeneousValidatorName = "cel.validator.homogeneous_literals" + nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit" + bindNestingLimitValidatorName = "cel.validator.bind_nesting_limit" + regexProgramSizeLimitValidatorName = "cel.validator.regex_program_size_limit" // HomogeneousAggregateLiteralExemptFunctions is the ValidatorConfig key used to configure // the set of function names which are exempt from homogeneous type checks. The expected type @@ -42,23 +46,37 @@ const ( ) var ( - astValidatorFactories = map[string]ASTValidatorFactory{ - nestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) { - if limit, found := val.ConfigValue("limit"); found { - // In case of protos, config value is of type by google.protobuf.Value, which numeric values are always a double. - if val, isDouble := limit.(float64); isDouble { - if val != float64(int64(val)) { - return nil, fmt.Errorf("invalid validator: %s, limit value is not a whole number: %v", nestingLimitValidatorName, limit) - } - return ValidateComprehensionNestingLimit(int(val)), nil - } + astValidatorFactoriesOnce sync.Once + astValidatorFactoriesMap map[string]ASTValidatorFactory +) - if val, isInt := limit.(int); isInt { - return ValidateComprehensionNestingLimit(val), nil - } - return nil, fmt.Errorf("invalid validator: %s unsupported limit type: %v", nestingLimitValidatorName, limit) +func astValidatorFactories() map[string]ASTValidatorFactory { + astValidatorFactoriesOnce.Do(initASTValidatorFactories) + return astValidatorFactoriesMap +} + +func initASTValidatorFactories() { + astValidatorFactoriesMap = map[string]ASTValidatorFactory{ + nestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) { + limit, err := validatorIntConfig(val, "limit") + if err != nil { + return nil, err } - return nil, fmt.Errorf("invalid validator: %s missing limit", nestingLimitValidatorName) + return ValidateComprehensionNestingLimit(limit), nil + }, + bindNestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) { + limit, err := validatorIntConfig(val, "limit") + if err != nil { + return nil, err + } + return ValidateBindNestingLimit(limit), nil + }, + regexProgramSizeLimitValidatorName: func(val *env.Validator) (ASTValidator, error) { + limit, err := validatorIntConfig(val, "limit") + if err != nil { + return nil, err + } + return ValidateRegexProgramSizeLimit(limit), nil }, durationValidatorName: func(*env.Validator) (ASTValidator, error) { return ValidateDurationLiterals(), nil @@ -73,19 +91,27 @@ var ( return ValidateHomogeneousAggregateLiterals(), nil }, } -) +} // ASTValidatorFactory creates an ASTValidator as configured by the input map type ASTValidatorFactory func(*env.Validator) (ASTValidator, error) // ASTValidators configures a set of ASTValidator instances into the target environment. // -// Validators are applied in the order in which the are specified and are treated as singletons. -// The same ASTValidator with a given name will not be applied more than once. +// Validators are applied in the order in which they are specified. +// If an ASTValidator with the same name is already configured, it will be replaced. func ASTValidators(validators ...ASTValidator) EnvOption { return func(e *Env) (*Env, error) { for _, v := range validators { - if !e.HasValidator(v.Name()) { + found := false + for i, existing := range e.validators { + if existing.Name() == v.Name() { + e.validators[i] = v + found = true + break + } + } + if !found { e.validators = append(e.validators, v) } } @@ -232,6 +258,18 @@ func ValidateComprehensionNestingLimit(limit int) ASTValidator { return nestingLimitValidator{limit: limit} } +// ValidateBindNestingLimit ensures that cel.bind() macro nesting does not exceed the specified limit. +// +// This validator can be useful for preventing arbitrarily nested cel.bind() macro calls. +func ValidateBindNestingLimit(limit int) ASTValidator { + return bindNestingLimitValidator{limit: limit} +} + +// ValidateRegexProgramSizeLimit ensures that regex pattern literals do not exceed the specified regex program size limit. +func ValidateRegexProgramSizeLimit(limit int) ASTValidator { + return regexProgramSizeLimitValidator{limit: limit} +} + type argChecker func(env *Env, call, arg ast.Expr) error func newFormatValidator(funcName string, argNum int, check argChecker) formatValidator { @@ -280,7 +318,7 @@ func (v formatValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Is func evalCall(env *Env, call, arg ast.Expr) error { ast := &Ast{impl: ast.NewAST(call, ast.NewSourceInfo(nil))} - prg, err := env.Program(ast) + prg, err := env.Program(ast, BypassAsyncProtection()) if err != nil { return err } @@ -430,8 +468,7 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i } // When the comprehension has an empty range, continue to the next ancestor // as this comprehension does not have any associated cost. - iterRange := e.AsComprehension().IterRange() - if iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 { + if isEmptyRangeComprehension(e) { e, hasParent = e.Parent() continue } @@ -445,3 +482,145 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i } } } + +type bindNestingLimitValidator struct { + limit int +} + +// Name returns the name of the cel.bind nesting limit validator. +func (v bindNestingLimitValidator) Name() string { + return bindNestingLimitValidatorName +} + +// ToConfig converts the ASTValidator to an env.Validator specifying the validator name and the nesting limit +// as an integer value: {"limit": int} +func (v bindNestingLimitValidator) ToConfig() *env.Validator { + return env.NewValidator(v.Name()).SetConfig(map[string]any{"limit": v.limit}) +} + +type regexProgramSizeLimitValidator struct { + limit int +} + +// Name returns the name of the regex program size limit validator. +func (v regexProgramSizeLimitValidator) Name() string { + return regexProgramSizeLimitValidatorName +} + +// ToConfig converts the ASTValidator to an env.Validator specifying the validator name and the limit. +func (v regexProgramSizeLimitValidator) ToConfig() *env.Validator { + return env.NewValidator(v.Name()).SetConfig(map[string]any{"limit": v.limit}) +} + +// Validate implements the ASTValidator interface method. +func (v bindNestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Issues) { + root := ast.NavigateAST(a) + comprehensions := ast.MatchDescendants(root, ast.KindMatcher(ast.ComprehensionKind)) + var celBinds []ast.NavigableExpr + for _, comp := range comprehensions { + if isCelBind(comp) { + celBinds = append(celBinds, comp) + } + } + if len(celBinds) <= v.limit { + return + } + for _, comp := range celBinds { + count := 0 + e := comp + hasParent := true + for hasParent { + if isCelBind(e) { + count++ + if count > v.limit { + iss.ReportErrorAtID(comp.ID(), "cel.bind exceeds nesting limit") + break + } + } + e, hasParent = e.Parent() + } + } +} + +func (v regexProgramSizeLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Issues) { + if v.limit <= 0 { + return + } + root := ast.NavigateAST(a) + callExprs := ast.MatchDescendants(root, ast.KindMatcher(ast.CallKind)) + for _, call := range callExprs { + c := call.AsCall() + fn := c.FunctionName() + if !isRegexFunctionName(fn) { + continue + } + args := c.Args() + var regexArgIndex int + if (fn == overloads.Matches || fn == "matches") && c.Target() != nil { + regexArgIndex = 0 + } else { + regexArgIndex = 1 + } + if len(args) <= regexArgIndex { + continue + } + arg := args[regexArgIndex] + if arg.Kind() != ast.LiteralKind { + continue + } + pattern, ok := arg.AsLiteral().Value().(string) + if !ok { + continue + } + sz, err := types.RegexProgramSize(pattern) + if err != nil { + // Invalid regex literals are handled in a different validator. + continue + } + if sz > v.limit { + iss.ReportErrorAtID(arg.ID(), "regex program size %d exceeds limit of %d", sz, v.limit) + } + } +} + +func isEmptyRangeComprehension(e ast.NavigableExpr) bool { + if e.Kind() != ast.ComprehensionKind { + return false + } + iterRange := e.AsComprehension().IterRange() + return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 +} + +func isCelBind(e ast.NavigableExpr) bool { + if !isEmptyRangeComprehension(e) { + return false + } + compre := e.AsComprehension() + loopCond := compre.LoopCondition() + loopStep := compre.LoopStep() + return compre.IterVar() == unusedIterVar && + loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral().Value() == false && + loopStep.Kind() == ast.IdentKind && loopStep.AsIdent() == compre.AccuVar() +} + +func isRegexFunctionName(fn string) bool { + return fn == overloads.Matches || fn == "matches" || fn == "regex.extract" || fn == "regex.extractAll" || fn == "regex.replace" +} + +func validatorIntConfig(val *env.Validator, configKey string) (int, error) { + if limit, found := val.ConfigValue(configKey); found { + // In case of protos, config value is of type google.protobuf.Value, which numeric values are always a double. + if v, isDouble := limit.(float64); isDouble { + if v != float64(int64(v)) { + return 0, fmt.Errorf("invalid validator: %s, %s value is not a whole number: %v", val.Name, configKey, limit) + } + return int(v), nil + } + + if v, isInt := limit.(int); isInt { + return v, nil + } + return 0, fmt.Errorf("invalid validator: %s unsupported %s type: %v", val.Name, configKey, limit) + } + return 0, fmt.Errorf("invalid validator: %s missing %s", val.Name, configKey) +} diff --git a/vendor/github.com/google/cel-go/checker/BUILD.bazel b/vendor/cel.dev/cel-go/checker/BUILD.bazel similarity index 92% rename from vendor/github.com/google/cel-go/checker/BUILD.bazel rename to vendor/cel.dev/cel-go/checker/BUILD.bazel index 678b412a955..73fc9a57a06 100644 --- a/vendor/github.com/google/cel-go/checker/BUILD.bazel +++ b/vendor/cel.dev/cel-go/checker/BUILD.bazel @@ -18,15 +18,17 @@ go_library( "scopes.go", "types.go", ], - importpath = "github.com/google/cel-go/checker", + importpath = "cel.dev/cel-go/checker", visibility = ["//visibility:public"], deps = [ "//checker/decls:go_default_library", "//common:go_default_library", "//common/ast:go_default_library", "//common/containers:go_default_library", + "//common/cost:go_default_library", "//common/debug:go_default_library", "//common/decls:go_default_library", + "//common/env:go_default_library", "//common/operators:go_default_library", "//common/overloads:go_default_library", "//common/stdlib:go_default_library", @@ -49,6 +51,7 @@ go_test( "cost_test.go", "env_test.go", "format_test.go", + "types_test.go", ], embed = [ ":go_default_library", diff --git a/vendor/github.com/google/cel-go/checker/checker.go b/vendor/cel.dev/cel-go/checker/checker.go similarity index 97% rename from vendor/github.com/google/cel-go/checker/checker.go rename to vendor/cel.dev/cel-go/checker/checker.go index 42d27a4283c..bc4e62d1282 100644 --- a/vendor/github.com/google/cel-go/checker/checker.go +++ b/vendor/cel.dev/cel-go/checker/checker.go @@ -22,13 +22,13 @@ import ( "slices" "strings" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) type checker struct { @@ -56,7 +56,7 @@ func Check(parsed *ast.AST, source common.Source, env *Env) (*ast.AST, *common.E AST: ast.NewCheckedAST(parsed, typeMap, refMap), ExprFactory: ast.NewExprFactory(), env: env, - errors: &typeErrors{errs: errs}, + errors: &typeErrors{errs: errs, env: env}, mappings: newMapping(), freeTypeVarCounter: 0, } @@ -152,6 +152,10 @@ func (c *checker) checkSelect(e ast.Expr) { e.SetKindCase(c.NewIdent(e.ID(), name)) return } + if c.errors.checkUndeclaredIdent(c.env, e.ID(), c.location(e), qualifiers...) { + c.setType(e, types.ErrorType) + return + } } resultType := c.checkSelectField(e, sel.Operand(), sel.FieldName(), false) @@ -307,6 +311,10 @@ func (c *checker) checkCall(e ast.Expr) { c.resolveOverloadOrError(e, fn, nil, args) return } + if c.errors.checkUndeclaredFunction(c.env, e.ID(), c.location(e), qualifiedPrefix, fnName) { + c.setType(e, types.ErrorType) + return + } } // Regular instance call. @@ -638,23 +646,11 @@ func (c *checker) newTypeVar() *types.Type { } func (c *checker) isAssignable(t1, t2 *types.Type) bool { - subs := isAssignable(c.mappings, t1, t2) - if subs != nil { - c.mappings = subs - return true - } - - return false + return isAssignable(c.mappings, t1, t2) } func (c *checker) isAssignableList(l1, l2 []*types.Type) bool { - subs := isAssignableList(c.mappings, l1, l2) - if subs != nil { - c.mappings = subs - return true - } - - return false + return isAssignableList(c.mappings, l1, l2) } func maybeUnwrapString(e ast.Expr) (string, bool) { diff --git a/vendor/cel.dev/cel-go/checker/cost.go b/vendor/cel.dev/cel-go/checker/cost.go new file mode 100644 index 00000000000..00a11511372 --- /dev/null +++ b/vendor/cel.dev/cel-go/checker/cost.go @@ -0,0 +1,115 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package checker + +import ( + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" +) + +type ( + // CostEstimator estimates the sizes of variable length input data and the costs of functions. + // + // Deprecated: use cost.CostEstimator + CostEstimator = cost.Estimator + + // CallEstimate includes a CostEstimate for the call, and an optional estimate of the result object size. + // + // Deprecated: use cost.CallEstimate + CallEstimate = cost.CallEstimate + + // AstNode represents an AST node for the purpose of cost estimations. + // + // Deprecated: use cost.AstNode + AstNode = cost.AstNode + + // SizeEstimate represents an estimated size of a variable length string, bytes, map or list. + // + // Deprecated: use cost.SizeEstimate + SizeEstimate = cost.SizeEstimate + + // CostEstimate represents an estimated cost range and provides add and multiply operations + // that do not overflow. + // + // Deprecated: use cost.CostEstimate + CostEstimate = cost.CostEstimate + + // CostOption configures flags which affect cost computations. + // + // Deprecated: use cost.CostOption + CostOption = cost.CostOption + + // FunctionEstimator provides a CallEstimate given the target and arguments for a specific function, overload pair. + // + // Deprecated: use cost.FunctionEstimator + FunctionEstimator = cost.FunctionEstimator +) + +// UnknownSizeEstimate returns a size between 0 and max uint. +// +// Deprecated: use cost.UnknownSizeEstimate +func UnknownSizeEstimate() SizeEstimate { + return cost.UnknownSizeEstimate() +} + +// FixedSizeEstimate returns a size estimate with a fixed min and max range. +// +// Deprecated: use cost.FixedSizeEstimate +func FixedSizeEstimate(size uint64) SizeEstimate { + return cost.FixedSizeEstimate(size) +} + +// UnknownCostEstimate returns a cost with an unknown impact. +// +// Deprecated: use cost.UnknownCostEstimate +func UnknownCostEstimate() CostEstimate { + return cost.UnknownCostEstimate() +} + +// FixedCostEstimate returns a cost with a fixed min and max range. +// +// Deprecated: use cost.FixedCostEstimate +func FixedCostEstimate(fixedCost uint64) CostEstimate { + return cost.FixedCostEstimate(fixedCost) +} + +// PresenceTestHasCost determines whether presence testing has a cost of one or zero. +// +// Deprecated: use cost.PresenceTestHasCost +func PresenceTestHasCost(hasCost bool) CostOption { + return cost.PresenceTestHasCost(hasCost) +} + +// OverloadCostEstimate binds a FunctionEstimator to a specific function overload ID. +// +// Deprecated: use cost.OverloadCostEstimate +func OverloadCostEstimate(overloadID string, functionCoster FunctionEstimator) CostOption { + return cost.OverloadCostEstimate(overloadID, functionCoster) +} + +// NewAstNode creates a new AstNode for cost estimation. +// +// Deprecated: use cost.NewAstNode +func NewAstNode(expr ast.Expr, path []string, t *types.Type, derivedSize *SizeEstimate) AstNode { + return cost.NewAstNode(expr, path, t, derivedSize) +} + +// Cost estimates the cost of the parsed and type checked CEL expression. +// +// Deprecated: use cost.Cost +func Cost(checked *ast.AST, estimator CostEstimator, opts ...CostOption) (CostEstimate, error) { + return cost.Cost(checked, estimator, opts...) +} diff --git a/vendor/github.com/google/cel-go/checker/decls/BUILD.bazel b/vendor/cel.dev/cel-go/checker/decls/BUILD.bazel similarity index 89% rename from vendor/github.com/google/cel-go/checker/decls/BUILD.bazel rename to vendor/cel.dev/cel-go/checker/decls/BUILD.bazel index a6b0be292ce..fe538016623 100644 --- a/vendor/github.com/google/cel-go/checker/decls/BUILD.bazel +++ b/vendor/cel.dev/cel-go/checker/decls/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "decls.go", ], - importpath = "github.com/google/cel-go/checker/decls", + importpath = "cel.dev/cel-go/checker/decls", deps = [ "@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", diff --git a/vendor/github.com/google/cel-go/checker/decls/decls.go b/vendor/cel.dev/cel-go/checker/decls/decls.go similarity index 100% rename from vendor/github.com/google/cel-go/checker/decls/decls.go rename to vendor/cel.dev/cel-go/checker/decls/decls.go diff --git a/vendor/github.com/google/cel-go/checker/env.go b/vendor/cel.dev/cel-go/checker/env.go similarity index 90% rename from vendor/github.com/google/cel-go/checker/env.go rename to vendor/cel.dev/cel-go/checker/env.go index 477918c4850..8acf806f0bd 100644 --- a/vendor/github.com/google/cel-go/checker/env.go +++ b/vendor/cel.dev/cel-go/checker/env.go @@ -17,12 +17,14 @@ package checker import ( "fmt" "strings" + "sync" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/env" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/parser" ) type aggregateLiteralElementType int @@ -75,6 +77,9 @@ type Env struct { aggLitElemType aggregateLiteralElementType filteredOverloadIDs map[string]struct{} jsonFieldNames bool + catalogSupplier CatalogSupplier + catalogOnce sync.Once + catalog *env.Catalog } // NewEnv returns a new *Env with the given parameters. @@ -97,7 +102,7 @@ func NewEnv(container *containers.Container, provider types.Provider, opts ...Op filteredOverloadIDs = make(map[string]struct{}) } if envOptions.validatedDeclarations != nil { - declarations = envOptions.validatedDeclarations.Copy() + declarations = envOptions.validatedDeclarations.PushInherited() } return &Env{ container: container, @@ -106,9 +111,23 @@ func NewEnv(container *containers.Container, provider types.Provider, opts ...Op aggLitElemType: aggLitElemType, filteredOverloadIDs: filteredOverloadIDs, jsonFieldNames: envOptions.jsonFieldNames, + catalogSupplier: envOptions.catalogSupplier, }, nil } +// Catalog returns the symbol catalog configured on the environment, or nil if none is configured. +func (e *Env) Catalog() *env.Catalog { + if e == nil { + return nil + } + e.catalogOnce.Do(func() { + if e.catalog == nil && e.catalogSupplier != nil { + e.catalog = e.catalogSupplier() + } + }) + return e.catalog +} + // AddIdents configures the checker with a list of variable declarations. // // If there are overlapping declarations, the method will error. @@ -320,26 +339,28 @@ func (e *Env) validatedDeclarations() *Scopes { return e.declarations } +// cloneWithDecls creates a copy of the Env with a new declaration scope stack. +func (e *Env) cloneWithDecls(decls *Scopes) *Env { + return &Env{ + container: e.container, + provider: e.provider, + declarations: decls, + aggLitElemType: e.aggLitElemType, + filteredOverloadIDs: e.filteredOverloadIDs, + jsonFieldNames: e.jsonFieldNames, + catalogSupplier: e.catalogSupplier, + catalog: e.catalog, + } +} + // enterScope creates a new Env instance with a new innermost declaration scope. func (e *Env) enterScope() *Env { - childDecls := e.declarations.Push() - return &Env{ - declarations: childDecls, - container: e.container, - provider: e.provider, - aggLitElemType: e.aggLitElemType, - } + return e.cloneWithDecls(e.declarations.Push()) } // exitScope creates a new Env instance with the nearest outer declaration scope. func (e *Env) exitScope() *Env { - parentDecls := e.declarations.Pop() - return &Env{ - declarations: parentDecls, - container: e.container, - provider: e.provider, - aggLitElemType: e.aggLitElemType, - } + return e.cloneWithDecls(e.declarations.Pop()) } // errorMsg is a type alias meant to represent error-based return values which diff --git a/vendor/github.com/google/cel-go/checker/errors.go b/vendor/cel.dev/cel-go/checker/errors.go similarity index 54% rename from vendor/github.com/google/cel-go/checker/errors.go rename to vendor/cel.dev/cel-go/checker/errors.go index 3535440baa4..970e7f420c4 100644 --- a/vendor/github.com/google/cel-go/checker/errors.go +++ b/vendor/cel.dev/cel-go/checker/errors.go @@ -15,14 +15,26 @@ package checker import ( - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" + "fmt" + "strings" + + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/env" + "cel.dev/cel-go/common/types" ) // typeErrors is a specialization of Errors. type typeErrors struct { errs *common.Errors + env *Env +} + +func (e *typeErrors) getCatalog() *env.Catalog { + if e == nil || e.env == nil { + return nil + } + return e.env.Catalog() } func (e *typeErrors) fieldTypeMismatch(id int64, l common.Location, name string, field, value *types.Type) { @@ -54,7 +66,11 @@ func (e *typeErrors) notAnOptionalFieldSelection(id int64, l common.Location, fi } func (e *typeErrors) notAType(id int64, l common.Location, typeName string) { - e.errs.ReportErrorAtID(id, l, "'%s' is not a type", typeName) + suggestion := "" + if cat := e.getCatalog(); cat != nil { + suggestion = formatSuggestionSuffix(typeName, cat.Find(typeName)) + } + e.errs.ReportErrorAtID(id, l, "'%s' is not a type%s", typeName, suggestion) } func (e *typeErrors) notAMessageType(id int64, l common.Location, typeName string) { @@ -80,7 +96,86 @@ func (e *typeErrors) undefinedField(id int64, l common.Location, field string) { } func (e *typeErrors) undeclaredReference(id int64, l common.Location, container string, name string) { - e.errs.ReportErrorAtID(id, l, "undeclared reference to '%s' (in container '%s')", name, container) + var syms []*env.CatalogSymbol + if cat := e.getCatalog(); cat != nil { + syms = cat.Find(name) + } + e.undeclaredReferenceWithSymbols(id, l, container, name, syms) +} + +func (e *typeErrors) undeclaredReferenceWithSymbols(id int64, l common.Location, container string, name string, syms []*env.CatalogSymbol) { + containerSuffix := "" + if container != "" { + containerSuffix = fmt.Sprintf(" (in container '%s')", container) + } + suggestion := formatSuggestionSuffix(name, syms) + e.errs.ReportErrorAtID(id, l, "undeclared reference to '%s'%s%s", name, containerSuffix, suggestion) +} + +func (e *typeErrors) checkUndeclaredIdent(env *Env, id int64, l common.Location, qualifiers ...string) bool { + if len(qualifiers) <= 1 || e.hasDeclaredPrefix(env, qualifiers[:len(qualifiers)-1]...) { + return false + } + return e.checkUndeclaredReference(env, id, l, strings.Join(qualifiers, ".")) +} + +func (e *typeErrors) checkUndeclaredFunction(env *Env, id int64, l common.Location, qualifiedPrefix, fnName string) bool { + prefixParts := strings.Split(qualifiedPrefix, ".") + if e.hasDeclaredPrefix(env, prefixParts...) { + return false + } + return e.checkUndeclaredReference(env, id, l, qualifiedPrefix+"."+fnName) +} + +func (e *typeErrors) checkUndeclaredReference(env *Env, id int64, l common.Location, qualifiedName string) bool { + if env == nil { + return false + } + cat := env.Catalog() + if cat == nil { + return false + } + syms := cat.Find(qualifiedName) + if len(syms) == 0 { + return false + } + container := "" + if env.container != nil { + container = env.container.Name() + } + e.undeclaredReferenceWithSymbols(id, l, container, qualifiedName, syms) + return true +} + +func (e *typeErrors) hasDeclaredPrefix(env *Env, qualifierPrefixes ...string) bool { + if env == nil { + return false + } + for i := 1; i <= len(qualifierPrefixes); i++ { + if env.resolveQualifiedIdent(qualifierPrefixes[:i]...) != nil { + return true + } + } + return false +} + +func formatSuggestionSuffix(name string, syms []*env.CatalogSymbol) string { + if len(syms) == 0 { + return "" + } + if len(syms) == 1 && syms[0].Name == name { + if syms[0].Option != "" { + return fmt.Sprintf(" (enable with `%s`)", syms[0].Option) + } + return "" + } + if len(syms) == 1 { + if syms[0].Option != "" { + return fmt.Sprintf(" (did you mean '%s'?, enable with `%s`)", syms[0].Name, syms[0].Option) + } + return fmt.Sprintf(" (did you mean '%s'?)", syms[0].Name) + } + return fmt.Sprintf(" (did you mean '%s' or '%s'?)", syms[0].Name, syms[1].Name) } func (e *typeErrors) unexpectedFailedResolution(id int64, l common.Location, typeName string) { diff --git a/vendor/github.com/google/cel-go/checker/format.go b/vendor/cel.dev/cel-go/checker/format.go similarity index 98% rename from vendor/github.com/google/cel-go/checker/format.go rename to vendor/cel.dev/cel-go/checker/format.go index 95842905e6d..17b1a4d24d5 100644 --- a/vendor/github.com/google/cel-go/checker/format.go +++ b/vendor/cel.dev/cel-go/checker/format.go @@ -18,8 +18,8 @@ import ( "fmt" "strings" - chkdecls "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common/types" + chkdecls "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common/types" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) diff --git a/vendor/github.com/google/cel-go/checker/mapping.go b/vendor/cel.dev/cel-go/checker/mapping.go similarity index 54% rename from vendor/github.com/google/cel-go/checker/mapping.go rename to vendor/cel.dev/cel-go/checker/mapping.go index 8163a908a5f..2fd506ea5d5 100644 --- a/vendor/github.com/google/cel-go/checker/mapping.go +++ b/vendor/cel.dev/cel-go/checker/mapping.go @@ -15,11 +15,21 @@ package checker import ( - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/types" ) type mapping struct { mapping map[string]*types.Type + // undo records what each add replaced while an attempt is in progress, + // so that a failed attempt can be rolled back. + undo []undoEntry + recording bool +} + +type undoEntry struct { + key string + prev *types.Type + had bool } func newMapping() *mapping { @@ -29,7 +39,32 @@ func newMapping() *mapping { } func (m *mapping) add(from, to *types.Type) { - m.mapping[FormatCELType(from)] = to + key := FormatCELType(from) + if m.recording { + prev, had := m.mapping[key] + m.undo = append(m.undo, undoEntry{key, prev, had}) + } + m.mapping[key] = to +} + +// attempt runs f against m in place, rolling back what f added if it reports +// false. Attempts do not nest. +func (m *mapping) attempt(f func() bool) bool { + m.recording = true + ok := f() + m.recording = false + if !ok { + for i := len(m.undo) - 1; i >= 0; i-- { + e := m.undo[i] + if e.had { + m.mapping[e.key] = e.prev + } else { + delete(m.mapping, e.key) + } + } + } + m.undo = m.undo[:0] + return ok } func (m *mapping) find(from *types.Type) (*types.Type, bool) { @@ -38,12 +73,3 @@ func (m *mapping) find(from *types.Type) (*types.Type, bool) { } return nil, false } - -func (m *mapping) copy() *mapping { - c := newMapping() - - for k, v := range m.mapping { - c.mapping[k] = v - } - return c -} diff --git a/vendor/github.com/google/cel-go/checker/options.go b/vendor/cel.dev/cel-go/checker/options.go similarity index 74% rename from vendor/github.com/google/cel-go/checker/options.go rename to vendor/cel.dev/cel-go/checker/options.go index af714323bae..2fae7acd5d9 100644 --- a/vendor/github.com/google/cel-go/checker/options.go +++ b/vendor/cel.dev/cel-go/checker/options.go @@ -14,16 +14,32 @@ package checker +import ( + "cel.dev/cel-go/common/env" +) + +// CatalogSupplier returns a Catalog of symbols. +type CatalogSupplier func() *env.Catalog + type options struct { crossTypeNumericComparisons bool homogeneousAggregateLiterals bool validatedDeclarations *Scopes jsonFieldNames bool + catalogSupplier CatalogSupplier } // Option is a functional option for configuring the type-checker type Option func(*options) error +// Catalog configures the checker with a symbol catalog supplier used for error suggestions and refinement. +func Catalog(cat CatalogSupplier) Option { + return func(opts *options) error { + opts.catalogSupplier = cat + return nil + } +} + // CrossTypeNumericComparisons toggles type-checker support for numeric comparisons across type // See https://github.com/google/cel-spec/wiki/proposal-210 for more details. func CrossTypeNumericComparisons(enabled bool) Option { @@ -33,8 +49,8 @@ func CrossTypeNumericComparisons(enabled bool) Option { } } -// ValidatedDeclarations provides a references to validated declarations which will be copied -// into new checker instances. +// ValidatedDeclarations provides a reference to validated declarations which will be inherited +// as a parent scope without copying. func ValidatedDeclarations(env *Env) Option { return func(opts *options) error { opts.validatedDeclarations = env.validatedDeclarations() diff --git a/vendor/github.com/google/cel-go/checker/printer.go b/vendor/cel.dev/cel-go/checker/printer.go similarity index 95% rename from vendor/github.com/google/cel-go/checker/printer.go rename to vendor/cel.dev/cel-go/checker/printer.go index 7a3984f02ce..c90711e4cad 100644 --- a/vendor/github.com/google/cel-go/checker/printer.go +++ b/vendor/cel.dev/cel-go/checker/printer.go @@ -17,8 +17,8 @@ package checker import ( "sort" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/debug" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/debug" ) type semanticAdorner struct { diff --git a/vendor/github.com/google/cel-go/checker/scopes.go b/vendor/cel.dev/cel-go/checker/scopes.go similarity index 75% rename from vendor/github.com/google/cel-go/checker/scopes.go rename to vendor/cel.dev/cel-go/checker/scopes.go index 9ae9832e152..88b664af6b3 100644 --- a/vendor/github.com/google/cel-go/checker/scopes.go +++ b/vendor/cel.dev/cel-go/checker/scopes.go @@ -17,7 +17,7 @@ package checker import ( "strings" - "github.com/google/cel-go/common/decls" + "cel.dev/cel-go/common/decls" ) // Scopes represents nested Decl sets where the Scopes value contains a Groups containing all @@ -25,8 +25,9 @@ import ( // Each Groups value is a mapping of names to Decls in the ident and function namespaces. // Lookups are performed such that bindings in inner scopes shadow those in outer scopes. type Scopes struct { - parent *Scopes - scopes *Group + parent *Scopes + inherited *Scopes + scopes *Group } // newScopes creates a new, empty Scopes. @@ -37,19 +38,6 @@ func newScopes() *Scopes { } } -// Copy creates a copy of the current Scopes values, including a copy of its parent if non-nil. -func (s *Scopes) Copy() *Scopes { - cpy := newScopes() - if s == nil { - return cpy - } - if s.parent != nil { - cpy.parent = s.parent.Copy() - } - cpy.scopes = s.scopes.copy() - return cpy -} - // Push creates a new Scopes value which references the current Scope as its parent. func (s *Scopes) Push() *Scopes { return &Scopes{ @@ -58,6 +46,14 @@ func (s *Scopes) Push() *Scopes { } } +// PushInherited creates a new Scopes value which references the current Scope as its inherited parent. +func (s *Scopes) PushInherited() *Scopes { + return &Scopes{ + inherited: s, + scopes: newGroup(), + } +} + // Pop returns the parent Scopes value for the current scope, or the current scope if the parent // is nil. func (s *Scopes) Pop() *Scopes { @@ -74,20 +70,6 @@ func (s *Scopes) AddIdent(decl *decls.VariableDecl) { s.scopes.idents[decl.Name()] = decl } -// FindIdent finds the first ident Decl with a matching name in Scopes, or nil if one cannot be -// found. -// Note: The search is performed from innermost to outermost. -func (s *Scopes) FindIdent(name string) *decls.VariableDecl { - name = strings.TrimPrefix(name, ".") - if ident, found := s.scopes.idents[name]; found { - return ident - } - if s.parent != nil { - return s.parent.FindIdent(name) - } - return nil -} - // FindIdentInScope finds the first ident Decl with a matching name in the current Scopes value, or // nil if one does not exist. // Note: The search is only performed on the current scope and does not search outer scopes. @@ -116,7 +98,13 @@ func (s *Scopes) FindGlobalIdent(name string) *decls.VariableDecl { for scope.parent != nil { scope = scope.parent } - return scope.FindIdentInScope(name) + if ident := scope.FindIdentInScope(name); ident != nil { + return ident + } + if scope.inherited != nil { + return scope.inherited.FindGlobalIdent(name) + } + return nil } // SetFunction adds the function Decl to the current scope. @@ -134,7 +122,14 @@ func (s *Scopes) FindFunction(name string) *decls.FunctionDecl { return fn } if s.parent != nil { - return s.parent.FindFunction(name) + if fn := s.parent.FindFunction(name); fn != nil { + return fn + } + } + if s.inherited != nil { + if fn := s.inherited.FindFunction(name); fn != nil { + return fn + } } return nil } @@ -147,22 +142,6 @@ type Group struct { functions map[string]*decls.FunctionDecl } -// copy creates a new Group instance with a shallow copy of the variables and functions. -// If callers need to mutate the exprpb.Decl definitions for a Function, they should copy-on-write. -func (g *Group) copy() *Group { - cpy := &Group{ - idents: make(map[string]*decls.VariableDecl, len(g.idents)), - functions: make(map[string]*decls.FunctionDecl, len(g.functions)), - } - for n, id := range g.idents { - cpy.idents[n] = id - } - for n, fn := range g.functions { - cpy.functions[n] = fn - } - return cpy -} - // newGroup creates a new Group with empty maps for identifiers and functions. func newGroup() *Group { return &Group{ diff --git a/vendor/github.com/google/cel-go/checker/types.go b/vendor/cel.dev/cel-go/checker/types.go similarity index 81% rename from vendor/github.com/google/cel-go/checker/types.go rename to vendor/cel.dev/cel-go/checker/types.go index 4c65b2737cd..01d858d81b1 100644 --- a/vendor/github.com/google/cel-go/checker/types.go +++ b/vendor/cel.dev/cel-go/checker/types.go @@ -15,7 +15,7 @@ package checker import ( - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/types" ) // isDyn returns true if the input t is either type DYN or a well-known ANY message. @@ -90,12 +90,35 @@ func isEqualOrLessSpecific(t1, t2 *types.Type) bool { return isEqualOrLessSpecific(t1.Parameters()[0], t2.Parameters()[0]) && isEqualOrLessSpecific(t1.Parameters()[1], t2.Parameters()[1]) case types.TypeKind: - return true + p1Len, p2Len := len(t1.Parameters()), len(t2.Parameters()) + if p1Len > 0 && p2Len > 0 { + if p1Len != p2Len { + return false + } + return isEqualOrLessSpecific(t1.Parameters()[0], t2.Parameters()[0]) + } + return p1Len == 0 default: return t1.IsExactType(t2) } } +// hasTypeParam returns true if the type is a type parameter or contains any type parameters directly or transitively. +func hasTypeParam(t *types.Type) bool { + if t == nil { + return false + } + if t.Kind() == types.TypeParamKind { + return true + } + for _, param := range t.Parameters() { + if hasTypeParam(param) { + return true + } + } + return false +} + // / internalIsAssignable returns true if t1 is assignable to t2. func internalIsAssignable(m *mapping, t1, t2 *types.Type) bool { // Process type parameters. @@ -143,7 +166,28 @@ func internalIsAssignable(m *mapping, t1, t2 *types.Type) bool { // Struct types. return t2.IsAssignableType(t1) case types.TypeKind: - return kind2 == types.TypeKind + if kind2 != types.TypeKind { + return false + } + p1Len, p2Len := len(t1.Parameters()), len(t2.Parameters()) + if p1Len == 0 || p2Len == 0 { + return p2Len == 0 + } + if p1Len != p2Len { + return false + } + fromType := t1.Parameters()[0] + toType := t2.Parameters()[0] + // If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T), + // delegate to inner type unification to bind or validate type parameter substitutions. + // Returns true if the inner types structurally match, unify with an unbound type param, + // or conform to an existing binding in 'm'. Returns false on structural/kind mismatches + // (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings. + if hasTypeParam(fromType) || hasTypeParam(toType) { + return internalIsAssignable(m, fromType, toType) + } + // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list). + return true case types.OpaqueKind, types.ListKind, types.MapKind: return t1.Kind() == t2.Kind() && t1.TypeName() == t2.TypeName() && internalIsAssignableList(m, t1.Parameters(), t2.Parameters()) @@ -219,22 +263,16 @@ func isLegacyNullable(t *types.Type) bool { return false } -// isAssignable returns an updated type substitution mapping if t1 is assignable to t2. -func isAssignable(m *mapping, t1, t2 *types.Type) *mapping { - mCopy := m.copy() - if internalIsAssignable(mCopy, t1, t2) { - return mCopy - } - return nil +// isAssignable reports whether t1 is assignable to t2, adding the substitutions that make it so +// to m. If t1 is not assignable to t2, m is left as it was. +func isAssignable(m *mapping, t1, t2 *types.Type) bool { + return m.attempt(func() bool { return internalIsAssignable(m, t1, t2) }) } -// isAssignableList returns an updated type substitution mapping if l1 is assignable to l2. -func isAssignableList(m *mapping, l1, l2 []*types.Type) *mapping { - mCopy := m.copy() - if internalIsAssignableList(mCopy, l1, l2) { - return mCopy - } - return nil +// isAssignableList reports whether l1 is assignable to l2, adding the substitutions that make it +// so to m. If l1 is not assignable to l2, m is left as it was. +func isAssignableList(m *mapping, l1, l2 []*types.Type) bool { + return m.attempt(func() bool { return internalIsAssignableList(m, l1, l2) }) } // mostGeneral returns the more general of two types which are known to unify. diff --git a/vendor/github.com/google/cel-go/common/BUILD.bazel b/vendor/cel.dev/cel-go/common/BUILD.bazel similarity index 93% rename from vendor/github.com/google/cel-go/common/BUILD.bazel rename to vendor/cel.dev/cel-go/common/BUILD.bazel index 1b1b7914d65..40888847229 100644 --- a/vendor/github.com/google/cel-go/common/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/BUILD.bazel @@ -15,7 +15,7 @@ go_library( "location.go", "source.go", ], - importpath = "github.com/google/cel-go/common", + importpath = "cel.dev/cel-go/common", deps = [ "//common/runes:go_default_library", "@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library", diff --git a/vendor/github.com/google/cel-go/common/ast/BUILD.bazel b/vendor/cel.dev/cel-go/common/ast/BUILD.bazel similarity index 95% rename from vendor/github.com/google/cel-go/common/ast/BUILD.bazel rename to vendor/cel.dev/cel-go/common/ast/BUILD.bazel index 9824f57a9f8..036d7eb8e90 100644 --- a/vendor/github.com/google/cel-go/common/ast/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/ast/BUILD.bazel @@ -10,11 +10,12 @@ go_library( srcs = [ "ast.go", "conversion.go", + "equiv.go", "expr.go", "factory.go", "navigable.go", ], - importpath = "github.com/google/cel-go/common/ast", + importpath = "cel.dev/cel-go/common/ast", deps = [ "//common:go_default_library", "//common/types:go_default_library", @@ -31,6 +32,7 @@ go_test( srcs = [ "ast_test.go", "conversion_test.go", + "equiv_test.go", "expr_test.go", "navigable_test.go", ], diff --git a/vendor/github.com/google/cel-go/common/ast/ast.go b/vendor/cel.dev/cel-go/common/ast/ast.go similarity index 97% rename from vendor/github.com/google/cel-go/common/ast/ast.go rename to vendor/cel.dev/cel-go/common/ast/ast.go index 3ae2e1063b3..d8e84713a47 100644 --- a/vendor/github.com/google/cel-go/common/ast/ast.go +++ b/vendor/cel.dev/cel-go/common/ast/ast.go @@ -18,9 +18,9 @@ package ast import ( "slices" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // AST contains a protobuf expression and source info along with CEL-native type and reference information. @@ -172,6 +172,14 @@ func (a *AST) IDs() map[int64]bool { return visitor } +// NodeCount returns the total number of expression nodes in the AST, including macro calls. +func NodeCount(a *AST) int { + if a == nil { + return 0 + } + return len(a.IDs()) +} + // ClearUnusedIDs removes IDs not used in the AST or macro calls from SourceInfo. func (a *AST) ClearUnusedIDs() { ids := a.IDs() @@ -444,7 +452,9 @@ func (s *SourceInfo) Extensions() []Extension { // at least as great as the lowest minor version specified. func (s *SourceInfo) HasExtension(id string, minVersion ExtensionVersion) bool { for _, ext := range s.Extensions() { - return ext.ID == id && ext.Version.Major == minVersion.Major && ext.Version.Minor >= minVersion.Minor + if ext.ID == id && ext.Version.Major == minVersion.Major && ext.Version.Minor >= minVersion.Minor { + return true + } } return false } @@ -454,6 +464,11 @@ func (s *SourceInfo) AddExtension(ext Extension) { if s == nil { return } + for _, e := range s.extensions { + if e.ID == ext.ID && e.Version == ext.Version && slices.Equal(e.Components, ext.Components) { + return + } + } s.extensions = append(s.extensions, ext) } diff --git a/vendor/github.com/google/cel-go/common/ast/conversion.go b/vendor/cel.dev/cel-go/common/ast/conversion.go similarity index 99% rename from vendor/github.com/google/cel-go/common/ast/conversion.go rename to vendor/cel.dev/cel-go/common/ast/conversion.go index 380f8c11810..5d032cc1df8 100644 --- a/vendor/github.com/google/cel-go/common/ast/conversion.go +++ b/vendor/cel.dev/cel-go/common/ast/conversion.go @@ -19,8 +19,8 @@ import ( "google.golang.org/protobuf/proto" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" celpb "cel.dev/expr" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" diff --git a/vendor/cel.dev/cel-go/common/ast/equiv.go b/vendor/cel.dev/cel-go/common/ast/equiv.go new file mode 100644 index 00000000000..3f5b9edce29 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/ast/equiv.go @@ -0,0 +1,459 @@ +// Copyright 2024 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 ast + +import ( + "math" + "slices" + + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// EquivOption configures the structural equivalence comparison between ASTs or expressions. +type EquivOption func(*equivOptions) + +// EquivTypes configures the type maps to use for validating type equivalence between two expressions or ASTs. +func EquivTypes(aTypes, bTypes map[int64]*types.Type) EquivOption { + return func(opts *equivOptions) { + opts.aTypes = aTypes + opts.bTypes = bTypes + opts.checkTypes = aTypes != nil || bTypes != nil + } +} + +// EquivReferences configures the reference maps to use for validating reference equivalence between two expressions or ASTs. +func EquivReferences(aRefs, bRefs map[int64]*ReferenceInfo) EquivOption { + return func(opts *equivOptions) { + opts.aRefs = aRefs + opts.bRefs = bRefs + opts.checkRefs = aRefs != nil || bRefs != nil + } +} + +// EquivIgnoreIdentifiers configures whether comprehension variable names (accuVar, iterVar, iterVar2) +// should be ignored during equivalence comparison. +func EquivIgnoreIdentifiers(enabled ...bool) EquivOption { + return func(opts *equivOptions) { + if len(enabled) == 0 { + opts.ignoreIdentifiers = true + return + } + opts.ignoreIdentifiers = enabled[0] + } +} + +// EquivMacroCalls configures whether to compare expressions by their macro call metadata +// if present, rather than their expanded AST representation. +func EquivMacroCalls(enabled ...bool) EquivOption { + return func(opts *equivOptions) { + if len(enabled) == 0 { + opts.checkMacros = true + return + } + opts.checkMacros = enabled[0] + } +} + +// EquivMacros configures the macro call maps to use for validating macro call equivalence between two expressions or ASTs. +func EquivMacros(aMacros, bMacros map[int64]Expr) EquivOption { + return func(opts *equivOptions) { + opts.aMacros = aMacros + opts.bMacros = bMacros + opts.checkMacros = aMacros != nil || bMacros != nil + } +} + +type equivOptions struct { + aTypes map[int64]*types.Type + bTypes map[int64]*types.Type + aRefs map[int64]*ReferenceInfo + bRefs map[int64]*ReferenceInfo + aMacros map[int64]Expr + bMacros map[int64]Expr + checkTypes bool + checkRefs bool + checkMacros bool + ignoreIdentifiers bool +} + +// EquivAST determines whether two ASTs are structurally equivalent. +// +// The comparison ignores numeric IDs except for resolving references and types. +// If both inputs are *AST instances, type and reference metadata are automatically +// compared if present. +// +// Note: expressions may be functionally equivalent, but not structurally equivalent. +// Proof of functional equivalence may be checked using the cel-java formal verification +// package (https://github.com/google/cel-java/blob/develop/optimizer/src/main/java/dev/cel/optimizer/verifier/README.md). +func EquivAST(a, b *AST, opts ...EquivOption) bool { + if a == nil || b == nil { + return a == b + } + optState := &equivOptions{ + aTypes: a.TypeMap(), + bTypes: b.TypeMap(), + aRefs: a.ReferenceMap(), + bRefs: b.ReferenceMap(), + aMacros: a.SourceInfo().MacroCalls(), + bMacros: b.SourceInfo().MacroCalls(), + checkTypes: a.IsChecked() || b.IsChecked(), + checkRefs: len(a.ReferenceMap()) > 0 || len(b.ReferenceMap()) > 0, + } + + for _, opt := range opts { + opt(optState) + } + + state := &equivState{equivOptions: optState} + return state.exprEquiv(a.Expr(), b.Expr()) +} + +// EquivExpr determines whether two expressions are structurally equivalent. +// +// The comparison ignores numeric IDs except for resolving references and types +// if options are provided. +func EquivExpr(a, b Expr, opts ...EquivOption) bool { + optState := &equivOptions{} + for _, opt := range opts { + opt(optState) + } + state := &equivState{equivOptions: optState} + return state.exprEquiv(a, b) +} + +// Equiv returns true if two ASTs are structurally equivalent. +// +// If the ASTs are type-checked, type and reference information will be compared by default. +// Options can be supplied to customize type or reference comparison. +func (a *AST) Equiv(other *AST, opts ...EquivOption) bool { + return EquivAST(a, other, opts...) +} + +type equivState struct { + *equivOptions + ignored1 []string + ignored2 []string +} + +func (s *equivState) pushIgnored(comp1, comp2 ComprehensionExpr) int { + prev := len(s.ignored1) + if comp1.IterVar() != "" { + s.ignored1 = append(s.ignored1, comp1.IterVar()) + } + if comp1.HasIterVar2() { + s.ignored1 = append(s.ignored1, comp1.IterVar2()) + } + if comp1.AccuVar() != "" { + s.ignored1 = append(s.ignored1, comp1.AccuVar()) + } + + if comp2.IterVar() != "" { + s.ignored2 = append(s.ignored2, comp2.IterVar()) + } + if comp2.HasIterVar2() { + s.ignored2 = append(s.ignored2, comp2.IterVar2()) + } + if comp2.AccuVar() != "" { + s.ignored2 = append(s.ignored2, comp2.AccuVar()) + } + return prev +} + +func (s *equivState) popIgnored(prev int) { + s.ignored1 = s.ignored1[:prev] + s.ignored2 = s.ignored2[:prev] +} + +func (s *equivState) isIgnored(name1, name2 string) bool { + idx1 := lastIgnored(s.ignored1, name1) + idx2 := lastIgnored(s.ignored2, name2) + if idx1 >= 0 || idx2 >= 0 { + return idx1 == idx2 + } + return name1 == name2 +} + +func lastIgnored(stack []string, name string) int { + for i, s := range slices.Backward(stack) { + if s == name { + return i + } + } + return -1 +} + +func (s *equivState) exprEquiv(e1, e2 Expr) bool { + if e1 == nil || e2 == nil { + return e1 == e2 + } + if !s.typeAndRefEquiv(e1.ID(), e2.ID()) { + return false + } + if s.checkMacros { + m1, found1 := s.aMacros[e1.ID()] + m2, found2 := s.bMacros[e2.ID()] + if found1 != found2 { + return false + } + if found1 { + return s.exprEquiv(m1, m2) + } + } + if e1.Kind() != e2.Kind() { + return false + } + switch e1.Kind() { + case UnspecifiedExprKind: + return true + case IdentKind: + name1 := e1.AsIdent() + name2 := e2.AsIdent() + if s.ignoreIdentifiers { + return s.isIgnored(name1, name2) + } + return name1 == name2 + case LiteralKind: + return equivLiteral(e1.AsLiteral(), e2.AsLiteral()) + case SelectKind: + s1 := e1.AsSelect() + s2 := e2.AsSelect() + if s1.IsTestOnly() != s2.IsTestOnly() { + return false + } + if s1.FieldName() != s2.FieldName() { + return false + } + return s.exprEquiv(s1.Operand(), s2.Operand()) + case CallKind: + c1 := e1.AsCall() + c2 := e2.AsCall() + if c1.FunctionName() != c2.FunctionName() || c1.IsMemberFunction() != c2.IsMemberFunction() { + return false + } + args1 := c1.Args() + args2 := c2.Args() + if len(args1) != len(args2) { + return false + } + if c1.IsMemberFunction() { + if !s.exprEquiv(c1.Target(), c2.Target()) { + return false + } + } + + for i := range args1 { + if !s.exprEquiv(args1[i], args2[i]) { + return false + } + } + return true + case ListKind: + l1 := e1.AsList() + l2 := e2.AsList() + if l1.Size() != l2.Size() { + return false + } + if len(l1.OptionalIndices()) != len(l2.OptionalIndices()) { + return false + } + elems1 := l1.Elements() + elems2 := l2.Elements() + for i := range elems1 { + if l1.IsOptional(int32(i)) != l2.IsOptional(int32(i)) { + return false + } + if !s.exprEquiv(elems1[i], elems2[i]) { + return false + } + } + return true + case MapKind: + m1 := e1.AsMap() + m2 := e2.AsMap() + if m1.Size() != m2.Size() { + return false + } + entries1 := m1.Entries() + entries2 := m2.Entries() + for i := range entries1 { + if !s.entryExprEquiv(entries1[i], entries2[i]) { + return false + } + } + return true + case StructKind: + st1 := e1.AsStruct() + st2 := e2.AsStruct() + if st1.TypeName() != st2.TypeName() { + return false + } + fields1 := st1.Fields() + fields2 := st2.Fields() + if len(fields1) != len(fields2) { + return false + } + for i := range fields1 { + if !s.entryExprEquiv(fields1[i], fields2[i]) { + return false + } + } + return true + case ComprehensionKind: + comp1 := e1.AsComprehension() + comp2 := e2.AsComprehension() + if comp1.HasIterVar2() != comp2.HasIterVar2() { + return false + } + if (comp1.IterVar() == "") != (comp2.IterVar() == "") || + (comp1.AccuVar() == "") != (comp2.AccuVar() == "") { + return false + } + if !s.ignoreIdentifiers { + if comp1.IterVar() != comp2.IterVar() || + comp1.IterVar2() != comp2.IterVar2() || + comp1.AccuVar() != comp2.AccuVar() { + return false + } + } + if !s.exprEquiv(comp1.IterRange(), comp2.IterRange()) || + !s.exprEquiv(comp1.AccuInit(), comp2.AccuInit()) { + return false + } + var prev int + if s.ignoreIdentifiers { + prev = s.pushIgnored(comp1, comp2) + } + equiv := s.exprEquiv(comp1.LoopCondition(), comp2.LoopCondition()) && + s.exprEquiv(comp1.LoopStep(), comp2.LoopStep()) && + s.exprEquiv(comp1.Result(), comp2.Result()) + if s.ignoreIdentifiers { + s.popIgnored(prev) + } + return equiv + default: + return false + } +} + +func (s *equivState) entryExprEquiv(e1, e2 EntryExpr) bool { + if e1 == nil || e2 == nil { + return e1 == e2 + } + if e1.Kind() != e2.Kind() { + return false + } + if !s.typeAndRefEquiv(e1.ID(), e2.ID()) { + return false + } + switch e1.Kind() { + case MapEntryKind: + me1 := e1.AsMapEntry() + me2 := e2.AsMapEntry() + if me1.IsOptional() != me2.IsOptional() { + return false + } + return s.exprEquiv(me1.Key(), me2.Key()) && s.exprEquiv(me1.Value(), me2.Value()) + case StructFieldKind: + sf1 := e1.AsStructField() + sf2 := e2.AsStructField() + if sf1.IsOptional() != sf2.IsOptional() { + return false + } + if sf1.Name() != sf2.Name() { + return false + } + return s.exprEquiv(sf1.Value(), sf2.Value()) + default: + return true + } +} + +func (s *equivState) typeAndRefEquiv(id1, id2 int64) bool { + if s.checkTypes { + t1, found1 := s.aTypes[id1] + t2, found2 := s.bTypes[id2] + if found1 != found2 { + return false + } + if found1 { + if t1 == nil || t2 == nil { + return t1 == t2 + } + if !t1.IsEquivalentType(t2) { + return false + } + } + } + if s.checkRefs { + r1, found1 := s.aRefs[id1] + r2, found2 := s.bRefs[id2] + if found1 != found2 { + return false + } + if found1 && !s.refEquiv(r1, r2) { + return false + } + } + return true +} + +func (s *equivState) refEquiv(r1, r2 *ReferenceInfo) bool { + if r1 == nil || r2 == nil { + return r1 == r2 + } + if s.ignoreIdentifiers { + if !s.isIgnored(r1.Name, r2.Name) { + return false + } + } else if r1.Name != r2.Name { + return false + } + if len(r1.OverloadIDs) != len(r2.OverloadIDs) { + return false + } + if len(r1.OverloadIDs) != 0 { + overloadMap := make(map[string]struct{}, len(r1.OverloadIDs)) + for _, id := range r1.OverloadIDs { + overloadMap[id] = struct{}{} + } + for _, id := range r2.OverloadIDs { + if _, found := overloadMap[id]; !found { + return false + } + } + } + if r1.Value == nil || r2.Value == nil { + return r1.Value == r2.Value + } + return r1.Value.Equal(r2.Value) == types.True +} + +func equivLiteral(l1, l2 ref.Val) bool { + if l1 == nil || l2 == nil { + return l1 == l2 + } + if l1.Type().TypeName() != l2.Type().TypeName() { + return false + } + if d1, ok := l1.(types.Double); ok { + if d2, ok := l2.(types.Double); ok { + if math.IsNaN(float64(d1)) && math.IsNaN(float64(d2)) { + return true + } + } + } + return l1.Equal(l2) == types.True +} diff --git a/vendor/github.com/google/cel-go/common/ast/expr.go b/vendor/cel.dev/cel-go/common/ast/expr.go similarity index 99% rename from vendor/github.com/google/cel-go/common/ast/expr.go rename to vendor/cel.dev/cel-go/common/ast/expr.go index 9f55cb3b9fc..459311f0d99 100644 --- a/vendor/github.com/google/cel-go/common/ast/expr.go +++ b/vendor/cel.dev/cel-go/common/ast/expr.go @@ -15,7 +15,7 @@ package ast import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // ExprKind represents the expression node kind. diff --git a/vendor/github.com/google/cel-go/common/ast/factory.go b/vendor/cel.dev/cel-go/common/ast/factory.go similarity index 99% rename from vendor/github.com/google/cel-go/common/ast/factory.go rename to vendor/cel.dev/cel-go/common/ast/factory.go index d4dcde4d947..b83e5555a82 100644 --- a/vendor/github.com/google/cel-go/common/ast/factory.go +++ b/vendor/cel.dev/cel-go/common/ast/factory.go @@ -14,7 +14,7 @@ package ast -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // ExprFactory interfaces defines a set of methods necessary for building native expression values. type ExprFactory interface { diff --git a/vendor/github.com/google/cel-go/common/ast/navigable.go b/vendor/cel.dev/cel-go/common/ast/navigable.go similarity index 93% rename from vendor/github.com/google/cel-go/common/ast/navigable.go rename to vendor/cel.dev/cel-go/common/ast/navigable.go index 13e5777b50d..3a71f865f43 100644 --- a/vendor/github.com/google/cel-go/common/ast/navigable.go +++ b/vendor/cel.dev/cel-go/common/ast/navigable.go @@ -15,8 +15,8 @@ package ast import ( - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // NavigableExpr represents the base navigable expression value with methods to inspect the @@ -181,6 +181,29 @@ func PreOrderVisit(expr Expr, visitor Visitor) { visit(expr, visitor, preOrder, 0, 0) } +// ExceedsDepth determines whether the AST contains expressions nested deeper than the specified +// maxDepth. The root expression has depth 0, so a maxDepth of 250 permits expressions nested up +// to and including 250 levels deep. +// +// The traversal is bounded: it descends at most maxDepth+1 levels, so it remains safe to call on +// adversarially deep inputs that could otherwise exhaust the Go stack during later checking or +// planning. A non-positive maxDepth disables the check and returns false. +func ExceedsDepth(a *AST, maxDepth int) bool { + if a == nil || maxDepth <= 0 { + return false + } + exceedsDepth := false + visitor := NewExprVisitor(func(e Expr) { + if nav, ok := e.(NavigableExpr); ok && nav.Depth() >= maxDepth { + exceedsDepth = true + } + }) + // Bound the walk to maxDepth+1 levels so it never recurses past the first level that exceeds + // the limit, keeping the check itself safe on the deep inputs it guards against. + visit(NavigateAST(a), visitor, postOrder, 0, maxDepth+1) + return exceedsDepth +} + type visitOrder int const ( diff --git a/vendor/github.com/google/cel-go/common/containers/BUILD.bazel b/vendor/cel.dev/cel-go/common/containers/BUILD.bazel similarity index 89% rename from vendor/github.com/google/cel-go/common/containers/BUILD.bazel rename to vendor/cel.dev/cel-go/common/containers/BUILD.bazel index 81197f0641c..7def2f256b0 100644 --- a/vendor/github.com/google/cel-go/common/containers/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/containers/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "container.go", ], - importpath = "github.com/google/cel-go/common/containers", + importpath = "cel.dev/cel-go/common/containers", deps = [ "//common/ast:go_default_library", ], diff --git a/vendor/github.com/google/cel-go/common/containers/container.go b/vendor/cel.dev/cel-go/common/containers/container.go similarity index 99% rename from vendor/github.com/google/cel-go/common/containers/container.go rename to vendor/cel.dev/cel-go/common/containers/container.go index fcfcdfc3f49..2330e0764fb 100644 --- a/vendor/github.com/google/cel-go/common/containers/container.go +++ b/vendor/cel.dev/cel-go/common/containers/container.go @@ -21,7 +21,7 @@ import ( "strings" "unicode" - "github.com/google/cel-go/common/ast" + "cel.dev/cel-go/common/ast" ) var ( diff --git a/vendor/github.com/google/cel-go/common/cost.go b/vendor/cel.dev/cel-go/common/cost.go similarity index 78% rename from vendor/github.com/google/cel-go/common/cost.go rename to vendor/cel.dev/cel-go/common/cost.go index 5e24bd0f47a..125b60e06b7 100644 --- a/vendor/github.com/google/cel-go/common/cost.go +++ b/vendor/cel.dev/cel-go/common/cost.go @@ -16,25 +16,39 @@ package common const ( // SelectAndIdentCost is the cost of an operation that accesses an identifier or performs a select. + // + // Deprecated: use cost.SelectAndIdentCost SelectAndIdentCost = 1 // ConstCost is the cost of an operation that accesses a constant. + // + // Deprecated: use cost.ConstCost ConstCost = 0 // ListCreateBaseCost is the base cost of any operation that creates a new list. + // + // Deprecated: use cost.ListCreateBaseCost ListCreateBaseCost = 10 // MapCreateBaseCost is the base cost of any operation that creates a new map. + // + // Deprecated: use cost.MapCreateBaseCost MapCreateBaseCost = 30 // StructCreateBaseCost is the base cost of any operation that creates a new struct. + // + // Deprecated: use cost.StructCreateBaseCost StructCreateBaseCost = 40 // StringTraversalCostFactor is multiplied to a length of a string when computing the cost of traversing the entire // string once. + // + // Deprecated: use cost.StringTraversalCostFactor StringTraversalCostFactor = 0.1 - // RegexStringLengthCostFactor is multiplied ot the length of a regex string pattern when computing the cost of + // RegexStringLengthCostFactor is multiplied to the length of a regex string pattern when computing the cost of // applying the regex to a string of unit cost. + // + // Deprecated: use cost.RegexStringLengthCostFactor RegexStringLengthCostFactor = 0.25 ) diff --git a/vendor/cel.dev/cel-go/common/cost/BUILD.bazel b/vendor/cel.dev/cel-go/common/cost/BUILD.bazel new file mode 100644 index 00000000000..5c38b80dcfb --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/BUILD.bazel @@ -0,0 +1,67 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], # Apache 2.0 +) + +go_library( + name = "go_default_library", + srcs = [ + "aggregate_strategy.go", + "cost.go", + "default_strategy.go", + "estimator.go", + "helper.go", + "model.go", + "standard.go", + "strategy.go", + "tracker.go", + "validate.go", + "version.go", + ], + importpath = "cel.dev/cel-go/common/cost", + deps = [ + "//common/ast:go_default_library", + "//common/operators:go_default_library", + "//common/overloads:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + "//common/types/traits:go_default_library", + ], +) + +go_test( + name = "go_default_test", + size = "small", + srcs = [ + "aggregate_strategy_test.go", + "algebra_test.go", + "cost_bounds_test.go", + "cost_test.go", + "default_strategy_test.go", + "length_test.go", + "model_test.go", + "optional_cost_test.go", + "optional_test.go", + "standard_test.go", + "totals_test.go", + "tracker_test.go", + "validate_test.go", + "version_test.go", + ], + embed = [ + ":go_default_library", + ], + deps = [ + "//cel:go_default_library", + "//common/ast:go_default_library", + "//common/decls:go_default_library", + "//common/operators:go_default_library", + "//common/overloads:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + "//ext:go_default_library", + "//test/proto3pb:go_default_library", + ], +) diff --git a/vendor/cel.dev/cel-go/common/cost/aggregate_strategy.go b/vendor/cel.dev/cel-go/common/cost/aggregate_strategy.go new file mode 100644 index 00000000000..a8e77261828 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/aggregate_strategy.go @@ -0,0 +1,244 @@ +// 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 cost + +import ( + "math" + + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// AggregateSizingStrategy returns a SizingStrategy that computes recursive size estimates +// by following paths during cost estimation, and calculates actual runtime size using +// AggregateSize during cost tracking. +// +// The strategy pins stringUnitLength to 1, overriding the types package default, so that +// raw character and byte lengths reach the cost model unscaled. This is deliberate: sizing +// strategies report raw dimensions, and cost expressions own the conversion to cost units +// by applying factors such as StringTraversalCostFactor. +// +// Callers may supply additional SizeCalculatorOption values, which are applied after the +// pinned default. +// +// Warning: passing types.SizeCalculatorStringUnitLength with a value other than 1 overrides +// the pinned default and causes string costs to be discounted twice, once by the calculator +// and again by the cost factor in the cost expression. A unit length of 10 combined with +// StringTraversalCostFactor yields an effective 100x discount rather than 10x. +func AggregateSizingStrategy(opts ...types.SizeCalculatorOption) SizingStrategy { + if len(opts) == 0 { + return defaultAggregateSizing + } + defaultOpts := []types.SizeCalculatorOption{types.SizeCalculatorStringUnitLength(1)} + return &aggregateSizingStrategy{calc: types.NewSizeCalculator(append(defaultOpts, opts...)...)} +} + +type aggregateSizingStrategy struct { + calc *types.SizeCalculator +} + +// TrackSize calculates the actual runtime aggregate size of a value during cost tracking. +// +// An optional is 1 plus the aggregate size of the value it holds, and an empty optional is 1, as +// types.Optional reports it; see Optional. +func (a *aggregateSizingStrategy) TrackSize(ctx TrackContext, value ref.Val) (uint64, bool) { + if value == nil { + return 0, false + } + return uint64(a.calc.AggregateSize(value)), true +} + +// EstimateSize computes recursive aggregate size estimates for an AST node during cost estimation. +func (a *aggregateSizingStrategy) EstimateSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + if node == nil { + return SizeEstimate{}, false + } + if sz := node.ComputedSize(); sz != nil { + return *sz, true + } + if node.Expr() != nil && node.Expr().Kind() == ast.LiteralKind { + return FixedSizeEstimate(uint64(a.calc.AggregateSize(node.Expr().AsLiteral()))), true + } + if node.Type() == nil { + return estimateScalarOrFallback(ctx, node) + } + switch node.Type().Kind() { + case types.ListKind: + return estimateAggregateListSize(ctx, node) + case types.MapKind: + return estimateAggregateMapSize(ctx, node) + case types.DynKind, types.AnyKind, types.TypeParamKind: + return estimateAggregateDynSize(ctx, node) + default: + return estimateScalarOrFallback(ctx, node) + } +} + +// estimateAggregateDynSize calculates the aggregate size estimate for a dynamically typed AST node, +// folding nested container hints ("@items", "@keys", "@values") into the aggregate size up to the +// hinted depth. +func estimateAggregateDynSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + if node.Expr() != nil { + switch node.Expr().Kind() { + case ast.ListKind: + return estimateAggregateListSize(ctx, node) + case ast.MapKind: + return estimateAggregateMapSize(ctx, node) + case ast.CallKind: + if call := node.Expr().AsCall(); call.FunctionName() == operators.Conditional { + if u, ok := estimateConditionalSize(ctx, node.Type(), call.Args()); ok { + return u, true + } + } + } + } + var baseSz *SizeEstimate + if sz, ok := estimateScalarOrFallback(ctx, node); ok { + if sz.Elem != nil || sz.Key != nil { + return sz, true + } + baseSz = &sz + } + if len(node.Path()) > 0 { + if elemSz := resolveDynamicChild(ctx, node.Path(), "@items"); elemSz != nil { + if baseSz == nil { + return combineListSize(nil, elemSz) + } + return SizeEstimate{ + Min: min(baseSz.Min, SafeAdd(1, SafeMultiply(baseSz.Min, max(elemSz.Min, 1)))), + Max: max(baseSz.Max, SafeAdd(1, SafeMultiply(baseSz.Max, max(elemSz.Max, 1)))), + Elem: elemSz, + }, true + } + keySz := resolveDynamicChild(ctx, node.Path(), "@keys") + valSz := resolveDynamicChild(ctx, node.Path(), "@values") + if keySz != nil || valSz != nil { + if baseSz == nil { + return combineMapSize(nil, keySz, valSz) + } + minKey, maxKey, keySz := resolveElemBounds(keySz, baseSz.Max > 0) + minVal, maxVal, valSz := resolveElemBounds(valSz, baseSz.Max > 0) + return SizeEstimate{ + Min: min(baseSz.Min, SafeAdd(1, SafeMultiply(baseSz.Min, SafeAdd(minKey, minVal)))), + Max: max(baseSz.Max, SafeAdd(1, SafeMultiply(baseSz.Max, SafeAdd(max(maxKey, 1), max(maxVal, 1))))), + Key: keySz, + Elem: valSz, + }, true + } + } + if baseSz != nil { + return *baseSz, true + } + return SizeEstimate{}, false +} + +// estimateAggregateListSize computes list aggregate size: 1 (header) + listSize * elemSize. +// +// Calls (e.g. `+` or `? :`) already hold aggregate sub-expression sizes. For `+`, 1 is +// deducted to deduplicate headers: (1+L) + (1+R) - 1 = 1+L+R. +// Unhinted variable-width elements default to [1, math.MaxUint64]. +func estimateAggregateListSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + elemType := listElemType(node.Type()) + listSize, elemSize := estimateListExpr(ctx, node, elemType) + + // When node is a call expression, estimateListExpr already evaluated aggregate sizes of sub-expressions. + if node.Expr() != nil && node.Expr().Kind() == ast.CallKind && listSize != nil { + call := node.Expr().AsCall() + if call.FunctionName() == operators.Add && len(call.Args()) == 2 { + // Concatenation of two lists: (1 + left) + (1 + right) = 2 + left + right. + // Subtract 1 to correct for the single resulting list container header. + minVal := max(SafeSubtract(listSize.Min, 1), 1) + maxVal := max(SafeSubtract(listSize.Max, 1), 1) + res := RangedSizeEstimate(minVal, maxVal) + res.Elem = elemSize + return res, true + } + // For other calls (e.g. conditional branches), listSize is already the branch aggregate size. + listSize.Key = nil + return *listSize, true + } + + listSize, elemSize = resolveListSizes(ctx, node, elemType, listSize, elemSize) + // The list length may be unknown (listSize == nil) while elemSize is known—for example, + // when the list has fixed-width primitive elements (e.g. list where elemSize is [1, 1]) + // or when a size hint was provided on the "@items" subpath without a hint on the list itself. + // combineListSize returns an unknown container size [0, MaxUint64] while preserving Elem so + // downstream operations (such as indexing or comprehensions) can still use the known element size. + if listSize == nil { + return combineListSize(nil, elemSize) + } + minElem, maxElem, elemSize := resolveElemBounds(elemSize, listSize.Max > 0) + aggMin := SafeAdd(1, SafeMultiply(listSize.Min, minElem)) + aggMax := SafeAdd(1, SafeMultiply(listSize.Max, maxElem)) + return SizeEstimate{ + Min: aggMin, + Max: aggMax, + Elem: elemSize, + }, true +} + +// estimateAggregateMapSize computes map aggregate size: 1 (header) + mapSize * (keySize + valSize). +// +// Calls (e.g. `? :`) already hold aggregate sub-expression sizes. +// Unhinted variable-width keys/values default to [1, math.MaxUint64]. +func estimateAggregateMapSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + keyType, valType := mapKeyValueTypes(node.Type()) + mapSize, keySize, valSize := estimateMapExpr(ctx, node, keyType, valType) + + // For call expressions (e.g. conditional branches), mapSize is already the branch aggregate size. + if node.Expr() != nil && node.Expr().Kind() == ast.CallKind && mapSize != nil { + return *mapSize, true + } + + mapSize, keySize, valSize = resolveMapSizes(ctx, node, keyType, valType, mapSize, keySize, valSize) + // The map entry count may be unknown (mapSize == nil) while keySize or valSize is known + // (e.g. fixed-width key/value types like int, or hints provided only on "@keys"/"@values"). + // combineMapSize returns an unknown container size while preserving Key and Elem estimates. + if mapSize == nil { + return combineMapSize(nil, keySize, valSize) + } + minKey, maxKey, keySize := resolveElemBounds(keySize, mapSize.Max > 0) + minVal, maxVal, valSize := resolveElemBounds(valSize, mapSize.Max > 0) + entryMin := SafeAdd(minKey, minVal) + entryMax := SafeAdd(maxKey, maxVal) + aggMin := SafeAdd(1, SafeMultiply(mapSize.Min, entryMin)) + aggMax := SafeAdd(1, SafeMultiply(mapSize.Max, entryMax)) + return SizeEstimate{ + Min: aggMin, + Max: aggMax, + Key: keySize, + Elem: valSize, + }, true +} + +// resolveElemBounds returns the [min, max] bounds for a child element/key/value in aggregate size +// calculation. Every value has aggregate size at least 1, so each bound is at least 1 and a +// container's size is at least 1 plus its length. For variable-width elements with no hint +// (sz == nil), the upper bound is math.MaxUint64; if the parent container can be non-empty, sz is +// populated with UnknownSizeEstimate(). +func resolveElemBounds(sz *SizeEstimate, nonEmptyContainer bool) (uint64, uint64, *SizeEstimate) { + if sz != nil { + return max(sz.Min, 1), max(sz.Max, 1), sz + } + if nonEmptyContainer { + u := UnknownSizeEstimate() + sz = &u + } + return 1, math.MaxUint64, sz +} + +var defaultAggregateSizing SizingStrategy = &aggregateSizingStrategy{calc: types.NewSizeCalculator(types.SizeCalculatorStringUnitLength(1))} diff --git a/vendor/cel.dev/cel-go/common/cost/cost.go b/vendor/cel.dev/cel-go/common/cost/cost.go new file mode 100644 index 00000000000..c73fc38bcf2 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/cost.go @@ -0,0 +1,483 @@ +// 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 cost provides cost estimation, cost tracking, and saturating arithmetic. +// +// Costs and sizes are unsigned 64-bit values where math.MaxUint64 doubles as the representation +// of an unbounded, or unknown, quantity. Every operation in this package saturates at +// math.MaxUint64 rather than wrapping so that an unbounded input remains unbounded through any +// sequence of operations. +package cost + +import ( + "math" + + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" +) + +const ( + // SelectAndIdentCost is the cost of an operation that accesses an identifier or performs a select. + SelectAndIdentCost = 1 + + // ConstCost is the cost of an operation that accesses a constant. + ConstCost = 0 + + // ListCreateBaseCost is the base cost of any operation that creates a new list. + ListCreateBaseCost = 10 + + // MapCreateBaseCost is the base cost of any operation that creates a new map. + MapCreateBaseCost = 30 + + // StructCreateBaseCost is the base cost of any operation that creates a new struct. + StructCreateBaseCost = 40 + + // StringTraversalCostFactor is multiplied to a length of a string when computing the cost of traversing the entire + // string once. + StringTraversalCostFactor = 0.1 + + // RegexStringLengthCostFactor is multiplied to the length of a regex string pattern when computing the cost of + // applying the regex to a string of unit cost. + RegexStringLengthCostFactor = 0.25 +) + +var ( + // CallCostEstimate is the base cost estimate for an O(1) function call. + CallCostEstimate = FixedCostEstimate(1) + + // CallCost is the base cost for an O(1) function call. + CallCost = uint64(1) + + // ListAllocCost is the base cost estimate for allocating a list. + ListAllocCost = FixedCostEstimate(ListCreateBaseCost) + + // StringCostFactor is the cost factor for traversing a string once. + StringCostFactor = StringTraversalCostFactor +) + +// maxUint64AsFloat is the smallest float64 value greater than math.MaxUint64. +// +// Conversion of a float64 to a uint64 is undefined when the value is out of range, so float +// results are compared against this bound before conversion. +var maxUint64AsFloat = math.Ldexp(1.0, 64) + +// SafeAdd returns the sum of the input values, saturating at math.MaxUint64. +func SafeAdd(x, y uint64, rest ...uint64) uint64 { + sum := x + if y > 0 && sum > math.MaxUint64-y { + return math.MaxUint64 + } + sum += y + for _, r := range rest { + if r > 0 && sum > math.MaxUint64-r { + return math.MaxUint64 + } + sum += r + } + return sum +} + +// SafeSubtract returns the difference of x - y, saturating at zero. +// If x is saturated (math.MaxUint64), it remains saturated. +func SafeSubtract(x, y uint64) uint64 { + if x == math.MaxUint64 { + return math.MaxUint64 + } + if x > y { + return x - y + } + return 0 +} + +// SafeMultiply returns the product of the input values, saturating at math.MaxUint64. +func SafeMultiply(x, y uint64) uint64 { + if y != 0 && x > math.MaxUint64/y { + return math.MaxUint64 + } + return x * y +} + +// SafeMultiplyByFactor multiplies a value by a cost factor and returns the nearest integer +// value, rounded up, saturating at math.MaxUint64. +func SafeMultiplyByFactor(x uint64, factor float64) uint64 { + xFloat := float64(x) + if xFloat > 0 && factor > 0 && xFloat > math.MaxUint64/factor { + return math.MaxUint64 + } + return SafeCeil(xFloat * factor) +} + +// SafeCeil returns the smallest integer value greater than or equal to the input, saturating at +// math.MaxUint64 and flooring at zero. +// +// Negative and NaN inputs return zero. +func SafeCeil(x float64) uint64 { + if math.IsNaN(x) || x <= 0 { + return 0 + } + ceil := math.Ceil(x) + if ceil >= maxUint64AsFloat { + return math.MaxUint64 + } + return uint64(ceil) +} + +// SafeTrunc returns the integer part of the input, saturating at math.MaxUint64 and flooring at +// zero. +// +// # Negative inputs return zero, and NaN saturates to max uint64 +// +// Cost trackers which report a truncated cost should prefer this helper over a direct conversion, +// as conversion of an out-of-range float64 to a uint64 is platform-defined. +func SafeTrunc(x float64) uint64 { + if x <= 0 { + return 0 + } + if math.IsNaN(x) || x >= maxUint64AsFloat { + return math.MaxUint64 + } + return uint64(x) +} + +// SafeMultiplyByFactorTrunc multiplies a value by a cost factor and returns the integer part of +// the result, truncated, saturating at math.MaxUint64. +// +// This is the truncating counterpart to SafeMultiplyByFactor, which rounds up. +func SafeMultiplyByFactorTrunc(x uint64, factor float64) uint64 { + xFloat := float64(x) + if xFloat > 0 && factor > 0 && xFloat > math.MaxUint64/factor { + return math.MaxUint64 + } + return SafeTrunc(xFloat * factor) +} + +// SizeEstimate represents an estimated size of a variable length string, bytes, map or list. +type SizeEstimate struct { + // Min is the minimum estimated size (inclusive). + Min uint64 + + // Max is the maximum estimated size (inclusive). + Max uint64 + + // Key indicates the estimated size of a key, set if the size estimate is for a map. + Key *SizeEstimate + + // Elem indicates the estimated size of a value, set if size estimate is for a map or list. + Elem *SizeEstimate +} + +// UnknownSizeEstimate returns a size between 0 and max uint. +func UnknownSizeEstimate() SizeEstimate { + return unknownSizeEstimate +} + +// FixedSizeEstimate returns a size estimate with a fixed min and max range. +func FixedSizeEstimate(size uint64) SizeEstimate { + return SizeEstimate{Min: size, Max: size} +} + +// RangedSizeEstimate returns a size estimate bounded by min and max. +func RangedSizeEstimate(min, max uint64) SizeEstimate { + return SizeEstimate{Min: min, Max: max} +} + +// AtLeastOneSize returns a size estimate with min and max guaranteed to be at least 1. +func AtLeastOneSize(size SizeEstimate) SizeEstimate { + if size.Min == 0 { + size.Min = 1 + } + if size.Max == 0 { + size.Max = 1 + } + return size +} + +// ListSizeEstimate returns a SizeEstimate for a list with the given list length and element size. +func ListSizeEstimate(listSize SizeEstimate, elemSize SizeEstimate) SizeEstimate { + listSize.Key = nil + listSize.Elem = &elemSize + return listSize +} + +// MapSizeEstimate returns a SizeEstimate for a map with the given map size, key size, and value size. +func MapSizeEstimate(mapSize SizeEstimate, keySize SizeEstimate, valSize SizeEstimate) SizeEstimate { + mapSize.Key = &keySize + mapSize.Elem = &valSize + return mapSize +} + +// Add adds to another SizeEstimate and returns the sum. +// If add would result in an uint64 overflow, the result is math.MaxUint64. +func (se SizeEstimate) Add(sizeEstimate SizeEstimate) SizeEstimate { + res := SizeEstimate{ + Min: SafeAdd(se.Min, sizeEstimate.Min), + Max: SafeAdd(se.Max, sizeEstimate.Max), + } + res.Key = mergeSizeEstimatePtr(se.Key, sizeEstimate.Key) + res.Elem = mergeSizeEstimatePtr(se.Elem, sizeEstimate.Elem) + return res +} + +// Subtract subtracts another SizeEstimate and returns the difference, saturating at zero. +func (se SizeEstimate) Subtract(sizeEstimate SizeEstimate) SizeEstimate { + res := SizeEstimate{ + Min: SafeSubtract(se.Min, sizeEstimate.Max), + Max: SafeSubtract(se.Max, sizeEstimate.Min), + } + res.Key = mergeSizeEstimatePtr(se.Key, sizeEstimate.Key) + res.Elem = mergeSizeEstimatePtr(se.Elem, sizeEstimate.Elem) + return res +} + +// Multiply multiplies by another SizeEstimate and returns the product. +// If multiply would result in an uint64 overflow, the result is math.MaxUint64. +// +// The product carries the union of the operands' key and element sizes, as Add, Subtract and +// Union do: a length scaled by a factor describes the same kind of element it started with. An +// estimator whose result holds something other than its operands' elements — expanded strings, for +// instance — must state that with List or Map rather than let the operands' sizes be inherited. +func (se SizeEstimate) Multiply(sizeEstimate SizeEstimate) SizeEstimate { + res := SizeEstimate{ + Min: SafeMultiply(se.Min, sizeEstimate.Min), + Max: SafeMultiply(se.Max, sizeEstimate.Max), + } + res.Key = mergeSizeEstimatePtr(se.Key, sizeEstimate.Key) + res.Elem = mergeSizeEstimatePtr(se.Elem, sizeEstimate.Elem) + return res +} + +// MultiplyByCostFactor multiplies a SizeEstimate by a cost factor and returns the CostEstimate with the +// nearest integer of the result, rounded up. +func (se SizeEstimate) MultiplyByCostFactor(costPerUnit float64) CostEstimate { + return CostEstimate{ + Min: SafeMultiplyByFactor(se.Min, costPerUnit), + Max: SafeMultiplyByFactor(se.Max, costPerUnit), + } +} + +// MultiplyByCost multiplies by the cost and returns the product. +// If multiply would result in an uint64 overflow, the result is math.MaxUint64. +func (se SizeEstimate) MultiplyByCost(estimate CostEstimate) CostEstimate { + return CostEstimate{ + SafeMultiply(se.Min, estimate.Min), + SafeMultiply(se.Max, estimate.Max), + } +} + +// Union returns a SizeEstimate that encompasses both input SizeEstimate values. +func (se SizeEstimate) Union(size SizeEstimate) SizeEstimate { + result := se + if size.Min < result.Min { + result.Min = size.Min + } + if size.Max > result.Max { + result.Max = size.Max + } + result.Key = mergeSizeEstimatePtr(se.Key, size.Key) + result.Elem = mergeSizeEstimatePtr(se.Elem, size.Elem) + return result +} + +// mergeSizeEstimatePtr merges two optional SizeEstimate pointers using union. +func mergeSizeEstimatePtr(a, b *SizeEstimate) *SizeEstimate { + if a == nil { + return b + } + if b == nil { + return a + } + u := a.Union(*b) + return &u +} + +// AsCost converts a size estimate to an equivalent cost estimate. +func (se SizeEstimate) AsCost() CostEstimate { + return se.MultiplyByCostFactor(1) +} + +// IsUnknown returns true if the size estimate represents an unknown size (0 to math.MaxUint64 with no key or element sizes). +func (se SizeEstimate) IsUnknown() bool { + return se == unknownSizeEstimate +} + +// Estimate represents an estimated cost range and provides add and multiply operations +// that do not overflow. +type Estimate struct { + Min, Max uint64 +} + +// CostEstimate is an alias for Estimate. +// +// Deprecated: use Estimate. +type CostEstimate = Estimate + +// UnknownCostEstimate returns a cost with an unknown impact. +func UnknownCostEstimate() CostEstimate { + return unknownCostEstimate +} + +// IsUnknown returns true if the cost estimate represents an unknown cost (0 to math.MaxUint64). +func (ce CostEstimate) IsUnknown() bool { + return ce == unknownCostEstimate +} + +// FixedCostEstimate returns a cost with a fixed min and max range. +func FixedCostEstimate(fixedCost uint64) CostEstimate { + return CostEstimate{Min: fixedCost, Max: fixedCost} +} + +// RangedCostEstimate returns a cost estimate bounded by min and max. +func RangedCostEstimate(min, max uint64) CostEstimate { + return CostEstimate{Min: min, Max: max} +} + +// Add adds the costs and returns the sum. +// If add would result in an uint64 overflow for the min or max, the value is set to math.MaxUint64. +func (ce CostEstimate) Add(estimate CostEstimate) CostEstimate { + return CostEstimate{ + Min: SafeAdd(ce.Min, estimate.Min), + Max: SafeAdd(ce.Max, estimate.Max), + } +} + +// Multiply multiplies by the cost and returns the product. +// If multiply would result in an uint64 overflow, the result is math.MaxUint64. +func (ce CostEstimate) Multiply(estimate CostEstimate) CostEstimate { + return CostEstimate{ + Min: SafeMultiply(ce.Min, estimate.Min), + Max: SafeMultiply(ce.Max, estimate.Max), + } +} + +// MultiplyByCostFactor multiplies a CostEstimate by a cost factor and returns the CostEstimate with the +// nearest integer of the result, rounded up. +func (ce CostEstimate) MultiplyByCostFactor(costPerUnit float64) CostEstimate { + return CostEstimate{ + Min: SafeMultiplyByFactor(ce.Min, costPerUnit), + Max: SafeMultiplyByFactor(ce.Max, costPerUnit), + } +} + +// Union returns a CostEstimate that encompasses both input CostEstimates. +func (ce CostEstimate) Union(size CostEstimate) CostEstimate { + result := ce + if size.Min < result.Min { + result.Min = size.Min + } + if size.Max > result.Max { + result.Max = size.Max + } + return result +} + +// CallEstimate includes a CostEstimate for the call, and an optional estimate of the result object size. +// The ResultSize should only be provided if the call results in a map, list, string or bytes. +type CallEstimate struct { + CostEstimate + + ResultSize *SizeEstimate +} + +// NewCallEstimate creates a new CallEstimate with the given cost and optional result size. +func NewCallEstimate(cost CostEstimate, sz *SizeEstimate) *CallEstimate { + return &CallEstimate{CostEstimate: cost, ResultSize: sz} +} + +// NewListCallEstimate creates a CallEstimate for a call that allocates a list, adding CallCost and +// ListCreateBaseCost to work and setting ResultSize to resultSize. +func NewListCallEstimate(work CostEstimate, resultSize SizeEstimate) *CallEstimate { + return NewCallEstimate( + work.Add(FixedCostEstimate(CallCost+ListCreateBaseCost)), + &resultSize, + ) +} + +// ActualSize returns the size of the value for all traits.Sizer values, +// and returns a size of 1 for all other value types. +func ActualSize(value ref.Val) uint64 { + if sz, ok := value.(traits.Sizer); ok { + return uint64(sz.Size().(types.Int)) + } + return 1 +} + +// EstimateSize returns a SizeEstimate for the given node from its computed size, estimator, or unknown. +func EstimateSize(estimator Estimator, node AstNode) SizeEstimate { + if node == nil { + return UnknownSizeEstimate() + } + if l := node.ComputedSize(); l != nil { + return *l + } + if estimator != nil { + if l := estimator.EstimateSize(node); l != nil { + return *l + } + } + return UnknownSizeEstimate() +} + +// EstimateTraversal computes cost as a function of the size of the target object and whether the call allocates memory. +func EstimateTraversal(nodeSize SizeEstimate, costFactor float64, allocationCost *CostEstimate) (CostEstimate, *SizeEstimate) { + cost := nodeSize.MultiplyByCostFactor(costFactor) + if allocationCost != nil { + cost = cost.Add(*allocationCost) + } + return cost, &nodeSize +} + +// EstimateStringScan estimates cost for scanning a string. +func EstimateStringScan(sz SizeEstimate) (CostEstimate, *SizeEstimate) { + return EstimateTraversal(sz, StringCostFactor, nil) +} + +// EstimateListAlloc estimates cost for allocating a list. +func EstimateListAlloc(sz SizeEstimate, costFactor float64) (CostEstimate, *SizeEstimate) { + return EstimateTraversal(sz, costFactor, &ListAllocCost) +} + +// UintValue returns the value of a non-negative literal int or uint node as a uint64, or false if +// the node is not a non-negative integer literal. +func UintValue(node AstNode) (uint64, bool) { + if node == nil || node.Expr() == nil || node.Expr().Kind() != ast.LiteralKind { + return 0, false + } + switch v := node.Expr().AsLiteral().(type) { + case types.Int: + if v < types.IntZero { + return 0, false + } + return uint64(v), true + case types.Uint: + return uint64(v), true + default: + return 0, false + } +} + +// NodeAsUintValue returns the value of a literal int node as a uint64, or the default value if the +// node is not a non-negative int literal. +func NodeAsUintValue(node AstNode, defaultVal uint64) uint64 { + if val, ok := UintValue(node); ok { + return val + } + if node != nil && node.Expr() != nil && node.Expr().Kind() == ast.LiteralKind { + if v, ok := node.Expr().AsLiteral().(types.Int); ok && v < types.IntZero { + return 0 + } + } + return defaultVal +} diff --git a/vendor/cel.dev/cel-go/common/cost/default_strategy.go b/vendor/cel.dev/cel-go/common/cost/default_strategy.go new file mode 100644 index 00000000000..1e5394aa367 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/default_strategy.go @@ -0,0 +1,554 @@ +// 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 cost + +import ( + "slices" + + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// DefaultSizingStrategy returns the default SizingStrategy implementation. +func DefaultSizingStrategy() SizingStrategy { + return defaultSizing +} + +// defaultSizingStrategy is the default implementation of SizingStrategy. +type defaultSizingStrategy struct{} + +// EstimateSize computes the size estimate for an AST node during cost estimation. +func (defaultSizingStrategy) EstimateSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + if node == nil { + return SizeEstimate{}, false + } + if sz := node.ComputedSize(); sz != nil { + return *sz, true + } + if node.Expr() != nil { + if sz := computeExprSize(node.Expr()); sz != nil { + return *sz, true + } + } + if node.Type() == nil { + return estimateScalarOrFallback(ctx, node) + } + switch node.Type().Kind() { + case types.ListKind: + return estimateDefaultListSize(ctx, node) + case types.MapKind: + return estimateDefaultMapSize(ctx, node) + case types.DynKind, types.AnyKind, types.TypeParamKind: + return estimateDefaultDynSize(ctx, node) + default: + return estimateScalarOrFallback(ctx, node) + } +} + +// maxHintDepth bounds how many nested container hint levels ("@items", "@keys", "@values") +// dynamic type size estimation inspects. +const maxHintDepth = 32 + +// estimateDefaultListSize calculates the size and element size of a list AST node. +func estimateDefaultListSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + elemType := listElemType(node.Type()) + listSize, elemSize := estimateListExpr(ctx, node, elemType) + listSize, elemSize = resolveListSizes(ctx, node, elemType, listSize, elemSize) + return combineListSize(listSize, elemSize) +} + +// estimateDefaultMapSize calculates the size, key size, and value size of a map AST node. +func estimateDefaultMapSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + keyType, valType := mapKeyValueTypes(node.Type()) + mapSize, keySize, valSize := estimateMapExpr(ctx, node, keyType, valType) + mapSize, keySize, valSize = resolveMapSizes(ctx, node, keyType, valType, mapSize, keySize, valSize) + return combineMapSize(mapSize, keySize, valSize) +} + +// estimateDefaultDynSize calculates the size estimate for a dynamically typed AST node. +// For variable/path nodes, the nesting of hints ("@items", "@keys", "@values") determines +// the possible container depth; any level beyond the hinted depth remains nil (unknown). +func estimateDefaultDynSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + if node.Expr() != nil { + switch node.Expr().Kind() { + case ast.ListKind: + return estimateDefaultListSize(ctx, node) + case ast.MapKind: + return estimateDefaultMapSize(ctx, node) + case ast.CallKind: + if call := node.Expr().AsCall(); call.FunctionName() == operators.Conditional { + if u, ok := estimateConditionalSize(ctx, node.Type(), call.Args()); ok { + return u, true + } + } + } + } + var baseSz *SizeEstimate + if sz, ok := estimateScalarOrFallback(ctx, node); ok { + if sz.Elem != nil || sz.Key != nil { + return sz, true + } + baseSz = &sz + } + if len(node.Path()) > 0 { + if elemSz := resolveDynamicChild(ctx, node.Path(), "@items"); elemSz != nil { + return combineListSize(baseSz, elemSz) + } + keySz := resolveDynamicChild(ctx, node.Path(), "@keys") + valSz := resolveDynamicChild(ctx, node.Path(), "@values") + if keySz != nil || valSz != nil { + return combineMapSize(baseSz, keySz, valSz) + } + } + if baseSz != nil { + return *baseSz, true + } + return SizeEstimate{}, false +} + +// resolveDynamicChild probes a child hint subpath ("@items", "@keys", "@values") under a +// dynamic node, recursing only when the estimator provides an explicit hint at that subpath. +func resolveDynamicChild(ctx EstimateContext, parentPath []string, subPath string) *SizeEstimate { + if len(parentPath) == 0 || len(parentPath) >= maxHintDepth || ctx == nil || ctx.Estimator() == nil { + return nil + } + childPath := append(slices.Clone(parentPath), subPath) + childNode := NewAstNode(nil, childPath, types.DynType, nil) + if ctx.Estimator().EstimateSize(childNode) == nil { + return nil + } + sz := ctx.Size(childNode) + if sz.IsUnknown() { + return nil + } + return &sz +} + +// resolveListSizes resolves a list node's container size and element size using the estimator, +// subpath hints ("@items"), and type fallbacks when not already determined from the expression. +func resolveListSizes(ctx EstimateContext, node AstNode, elemType *types.Type, listSize, elemSize *SizeEstimate) (*SizeEstimate, *SizeEstimate) { + if listSize == nil && ctx != nil && ctx.Estimator() != nil { + listSize = ctx.Estimator().EstimateSize(node) + } + if elemSize == nil && listSize != nil { + elemSize = cloneSizeEstimate(listSize.Elem) + } + if listSize == nil || listSize.Max > 0 || (ctx != nil && versionOf(ctx) < 1) { + elemSize = resolveChildSize(ctx, node.Path(), "@items", elemType, elemSize) + } + // If neither the list length nor element size is known for an AST expression node + // (e.g. an unhinted list variable), default listSize to unknown so the + // strategy computes the worst-case bound. + if listSize == nil && elemSize == nil && node.Expr() != nil { + u := UnknownSizeEstimate() + listSize = &u + } + return listSize, elemSize +} + +// resolveMapSizes resolves a map node's container size, key size, and value size using the +// estimator, subpath hints ("@keys", "@values"), and type fallbacks. +func resolveMapSizes(ctx EstimateContext, node AstNode, keyType, valType *types.Type, mapSize, keySize, valSize *SizeEstimate) (*SizeEstimate, *SizeEstimate, *SizeEstimate) { + if mapSize == nil && ctx != nil && ctx.Estimator() != nil { + mapSize = ctx.Estimator().EstimateSize(node) + } + if mapSize != nil { + if keySize == nil { + keySize = cloneSizeEstimate(mapSize.Key) + } + if valSize == nil { + valSize = cloneSizeEstimate(mapSize.Elem) + } + } + // Query subpaths @keys and @values if hints were not provided on the parent map node. + if mapSize == nil || mapSize.Max > 0 || (ctx != nil && versionOf(ctx) < 1) { + keySize = resolveChildSize(ctx, node.Path(), "@keys", keyType, keySize) + valSize = resolveChildSize(ctx, node.Path(), "@values", valType, valSize) + } + // If no map, key, or value sizes are known for an AST expression node (e.g. an unhinted + // map), default mapSize to unknown to compute the worst-case bound. + if mapSize == nil && keySize == nil && valSize == nil && node.Expr() != nil { + u := UnknownSizeEstimate() + mapSize = &u + } + return mapSize, keySize, valSize +} + +// resolveChildSize looks up child size estimates on the given subPath (e.g. "@items", "@keys", "@values") +// when the current estimate is missing or lacks nested container details, falling back to primitive/type hints. +func resolveChildSize(ctx EstimateContext, parentPath []string, subPath string, childType *types.Type, current *SizeEstimate) *SizeEstimate { + if current == nil || (current.Elem == nil && (isContainerKind(childType.Kind()) || isDynamicKind(childType.Kind()))) { + if len(parentPath) > 0 && ctx != nil { + childPath := append(slices.Clone(parentPath), subPath) + sz := ctx.Size(NewAstNode(nil, childPath, childType, nil)) + if !sz.IsUnknown() { + if current == nil { + current = &sz + } else { + current.Elem = sz.Elem + current.Key = sz.Key + } + } + } + } + if current == nil { + current = fallbackElemSize(ctx, childType) + } + return current +} + +// isContainerKind reports whether kind is a list or map container type. +func isContainerKind(kind types.Kind) bool { + return kind == types.ListKind || kind == types.MapKind +} + +// isDynamicKind reports whether kind represents a dynamically typed value. +func isDynamicKind(kind types.Kind) bool { + return kind == types.DynKind || kind == types.AnyKind || kind == types.TypeParamKind +} + +// fallbackElemSize resolves fixed-width primitive type sizes via computeTypeSize, +// or queries the estimator with an untyped/pathless node for type-level hints. +func fallbackElemSize(ctx EstimateContext, elemType *types.Type) *SizeEstimate { + if sz := computeTypeSize(elemType, versionOf(ctx)); sz != nil { + return ownedSize(sz) + } + if ctx != nil && ctx.Estimator() != nil { + elemNode := NewAstNode(nil, nil, elemType, nil) + return ctx.Estimator().EstimateSize(elemNode) + } + return nil +} + +// cloneSizeEstimate makes a copy of a SizeEstimate to prevent in-place mutation of cached/shared estimates. +func cloneSizeEstimate(se *SizeEstimate) *SizeEstimate { + if se == nil { + return nil + } + clone := *se + return &clone +} + +// TrackSize computes the actual runtime size of a value. +func (defaultSizingStrategy) TrackSize(ctx TrackContext, value ref.Val) (uint64, bool) { + if value == nil { + return 0, false + } + if opt, isOpt := value.(*types.Optional); isOpt { + return optionalLength(trackVersionOf(ctx), opt), true + } + return ActualSize(value), true +} + +// optionalLength reports the size of an optional under the default sizing strategy, at the given +// cost model version. +// +// From version 1 an optional is 1 plus the length of the value it holds, so an empty optional is 1, +// and the optional's element is that value; see Optional. Version 0 sizes every optional as 1, as +// cel-go v0.32.0 charged it through ActualSize, for which an optional is not a traits.Sizer. +func optionalLength(version uint32, opt *types.Optional) uint64 { + if version < 1 || !opt.HasValue() { + return 1 + } + if inner, isOpt := opt.GetValue().(*types.Optional); isOpt { + return SafeAdd(1, optionalLength(version, inner)) + } + return SafeAdd(1, ActualSize(opt.GetValue())) +} + +// trackVersionContext is implemented by TrackContext values which know the cost model version they +// track under. It is unexported because TrackContext is an interface third parties may implement. +type trackVersionContext interface { + modelVersion() uint32 +} + +// trackVersionOf reports the cost model version of a TrackContext: the latest version for a context +// which does not report one. +func trackVersionOf(ctx TrackContext) uint32 { + if v, ok := ctx.(trackVersionContext); ok { + return v.modelVersion() + } + return defaultModelVersion +} + +// optionalValueOf reports whether value is an optional, and if so returns the value it holds, or +// nil when the optional is empty. +func optionalValueOf(value ref.Val) (ref.Val, bool) { + opt, isOpt := value.(*types.Optional) + if !isOpt { + return nil, false + } + if !opt.HasValue() { + return nil, true + } + return opt.GetValue(), true +} + +// listElemType extracts the element type of a list, defaulting to DynType if not parameterized. +func listElemType(t *types.Type) *types.Type { + if len(t.Parameters()) > 0 && t.Parameters()[0] != nil { + return t.Parameters()[0] + } + return types.DynType +} + +// mapKeyValueTypes extracts key and value types of a map, defaulting to DynType if not parameterized. +func mapKeyValueTypes(t *types.Type) (keyType, valType *types.Type) { + keyType = types.DynType + valType = types.DynType + if len(t.Parameters()) >= 2 { + keyType = t.Parameters()[0] + valType = t.Parameters()[1] + } + return keyType, valType +} + +// unionAccumulator merges a new size estimate into an existing accumulator pointer. +func unionAccumulator(acc *SizeEstimate, next SizeEstimate) *SizeEstimate { + if acc == nil { + return &next + } + u := acc.Union(next) + return &u +} + +// estimateConditionalSize computes the union SizeEstimate of the branches in a conditional call. +func estimateConditionalSize(ctx EstimateContext, nodeType *types.Type, args []ast.Expr) (SizeEstimate, bool) { + if len(args) == 3 { + tVal := ctx.Size(NewAstNode(args[1], nil, nodeType, nil)) + fVal := ctx.Size(NewAstNode(args[2], nil, nodeType, nil)) + return tVal.Union(fVal), true + } + return SizeEstimate{}, false +} + +// estimateListExpr computes the list and element size estimates for literal list expressions +// or container call operations (e.g. Add, Conditional). +func estimateListExpr(ctx EstimateContext, node AstNode, elemType *types.Type) (listSize *SizeEstimate, elemSize *SizeEstimate) { + if node.Expr() == nil { + return nil, nil + } + switch node.Expr().Kind() { + case ast.ListKind: + list := node.Expr().AsList() + elements := list.Elements() + l := FixedSizeEstimate(uint64(len(elements))) + // An optional element is omitted when it has no value, so only the required + // elements count toward the minimum length. + l.Min = uint64(len(elements) - len(list.OptionalIndices())) + optional := make(map[int32]bool, len(list.OptionalIndices())) + for _, i := range list.OptionalIndices() { + optional[i] = true + } + for i, elem := range elements { + if optional[int32(i)] { + // The list holds the optional's value, if any, rather than the optional. + var present bool + if elem, present = optionalLiteralValue(elem); !present { + l.Max = SafeSubtract(l.Max, 1) + continue + } + } + elemNode := NewAstNode(elem, nil, elemType, nil) + elemSize = unionAccumulator(elemSize, ctx.Size(elemNode)) + } + listSize = &l + case ast.CallKind: + call := node.Expr().AsCall() + args := call.Args() + switch call.FunctionName() { + case operators.Add: + if len(args) == 2 { + lhs := ctx.Size(NewAstNode(args[0], nil, node.Type(), nil)) + rhs := ctx.Size(NewAstNode(args[1], nil, node.Type(), nil)) + added := lhs.Add(rhs) + listSize = &added + elemSize = added.Elem + } + case operators.Conditional: + if u, ok := estimateConditionalSize(ctx, node.Type(), args); ok { + listSize = &u + elemSize = u.Elem + } + } + } + return listSize, elemSize +} + +// estimateMapExpr computes map, key, and value size estimates for literal map expressions +// or container call operations (e.g. Conditional). +func estimateMapExpr(ctx EstimateContext, node AstNode, keyType, valType *types.Type) (mapSize *SizeEstimate, keySize *SizeEstimate, valSize *SizeEstimate) { + if node.Expr() == nil { + return nil, nil, nil + } + switch node.Expr().Kind() { + case ast.MapKind: + entries := node.Expr().AsMap().Entries() + m := FixedSizeEstimate(uint64(len(entries))) + mapSize = &m + for _, entry := range entries { + mapEntry := entry.AsMapEntry() + if mapEntry.IsOptional() { + // An optional entry is omitted when its value is absent. + m.Min = SafeSubtract(m.Min, 1) + } + kNode := NewAstNode(mapEntry.Key(), nil, keyType, nil) + vNode := NewAstNode(mapEntry.Value(), nil, valType, nil) + keySize = unionAccumulator(keySize, ctx.Size(kNode)) + valSize = unionAccumulator(valSize, ctx.Size(vNode)) + } + case ast.CallKind: + call := node.Expr().AsCall() + switch call.FunctionName() { + case "cel.@mapInsert": + args := call.Args() + if len(args) == 3 { + lhs := ctx.Size(NewAstNode(args[0], nil, node.Type(), nil)) + kSz := ctx.Size(NewAstNode(args[1], nil, keyType, nil)) + vSz := ctx.Size(NewAstNode(args[2], nil, valType, nil)) + keySize = mergeSizeEstimatePtr(lhs.Key, &kSz) + valSize = mergeSizeEstimatePtr(lhs.Elem, &vSz) + added := lhs.Add(FixedSizeEstimate(1)) + added.Key = keySize + added.Elem = valSize + mapSize = &added + } else if len(args) == 2 { + lhs := ctx.Size(NewAstNode(args[0], nil, node.Type(), nil)) + rhs := ctx.Size(NewAstNode(args[1], nil, node.Type(), nil)) + added := lhs.Add(rhs) + mapSize = &added + keySize = added.Key + valSize = added.Elem + } + case operators.Conditional: + if u, ok := estimateConditionalSize(ctx, node.Type(), call.Args()); ok { + mapSize = &u + keySize = u.Key + valSize = u.Elem + } + } + } + return mapSize, keySize, valSize +} + +// estimateScalarOrFallback returns size estimates using custom estimator hints or primitive type sizes. +func estimateScalarOrFallback(ctx EstimateContext, node AstNode) (SizeEstimate, bool) { + if node != nil && node.Expr() != nil { + if sz := computeExprSize(node.Expr()); sz != nil { + return *sz, true + } + } + if ctx != nil && ctx.Estimator() != nil { + if sz := ctx.Estimator().EstimateSize(node); sz != nil { + return *sz, true + } + } + if node.Type() != nil { + if sz := computeTypeSize(node.Type(), versionOf(ctx)); sz != nil { + return *sz, true + } + } + return SizeEstimate{}, false +} + +// combineListSize constructs a list SizeEstimate combining container length and element size. +// +// A list that can be non-empty (Max > 0) whose element size is unknown carries an explicit +// UnknownSizeEstimate in Elem rather than nil, so combining it with a literal list (e.g. +// ['a'] + unhinted) widens the element size to unknown instead of dropping it to the literal's. +// Empty list literals (Max == 0) leave Elem nil so they contribute no element bound when joined. +func combineListSize(listSize, elemSize *SizeEstimate) (SizeEstimate, bool) { + if listSize != nil { + if elemSize == nil && listSize.Max > 0 { + u := UnknownSizeEstimate() + elemSize = &u + } + listSize.Key = nil + listSize.Elem = elemSize + return *listSize, true + } + if elemSize != nil { + res := UnknownSizeEstimate() + res.Elem = elemSize + return res, true + } + return SizeEstimate{}, false +} + +// combineMapSize constructs a map SizeEstimate combining map size, key size, and value size. +// +// As with combineListSize, a map that can be non-empty (Max > 0) records an explicit +// UnknownSizeEstimate for an unhinted Key or Elem so merging with a literal map widens the bound +// rather than dropping the unknown side. +func combineMapSize(mapSize, keySize, valSize *SizeEstimate) (SizeEstimate, bool) { + if mapSize != nil { + if mapSize.Max > 0 { + if keySize == nil { + u := UnknownSizeEstimate() + keySize = &u + } + if valSize == nil { + u := UnknownSizeEstimate() + valSize = &u + } + } + mapSize.Key = keySize + mapSize.Elem = valSize + return *mapSize, true + } + if keySize != nil || valSize != nil { + if keySize == nil { + u := UnknownSizeEstimate() + keySize = &u + } + if valSize == nil { + u := UnknownSizeEstimate() + valSize = &u + } + res := UnknownSizeEstimate() + res.Key = keySize + res.Elem = valSize + return res, true + } + return SizeEstimate{}, false +} + +var defaultSizing SizingStrategy = defaultSizingStrategy{} + +// optionalLiteralValue returns the value an optional list element contributes: nothing for +// optional.none(), the argument of optional.of(x) or optional.ofNonZeroValue(x), and otherwise +// the element itself, whose value is not known statically. +func optionalLiteralValue(e ast.Expr) (ast.Expr, bool) { + if e.Kind() != ast.CallKind { + return e, true + } + call := e.AsCall() + if call.IsMemberFunction() { + return e, true + } + switch call.FunctionName() { + case "optional.none": + if len(call.Args()) == 0 { + return nil, false + } + case "optional.of", "optional.ofNonZeroValue": + if len(call.Args()) == 1 { + return call.Args()[0], true + } + } + return e, true +} diff --git a/vendor/cel.dev/cel-go/common/cost/estimator.go b/vendor/cel.dev/cel-go/common/cost/estimator.go new file mode 100644 index 00000000000..69e25f12d00 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/estimator.go @@ -0,0 +1,1260 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cost + +import ( + "fmt" + "math" + "strconv" + + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" +) + +// Estimator provides CallCost and Size estimates for cost calculation. +type Estimator interface { + + // EstimateCallCost returns the CallEstimate for a function, overload and target and arguments. + // + // If nil is returned, the cost of the function is calculated by the OverloadCostEstimate if + // provided, or standard CEL function cost estimation. + EstimateCallCost(function, overloadID string, target *AstNode, args []AstNode) *CallEstimate + + // EstimateSize returns the SizeEstimate of an AstNode. + // + // If nil is returned, the size of the node is calculated by the SizingStrategy. + EstimateSize(element AstNode) *SizeEstimate +} + +// AstNode represents an AST node for estimating cost. +type AstNode interface { + // Path returns a field path through the provided type declarations to the type of the AstNode, or nil if the AstNode does not + // represent type directly reachable from the provided type declarations. + // The first path element is a variable. All subsequent path elements are one of: field name, '@items', '@keys', '@values'. + Path() []string + + // Type returns the deduced type of the AstNode. + Type() *types.Type + + // Expr returns the expression of the AstNode. + Expr() ast.Expr + + // ComputedSize returns a size estimate of the AstNode derived from information available in the CEL expression. + // For constants and inline list and map declarations, the exact size is returned. For concatenated list, strings + // and bytes, the size is derived from the size estimates of the operands. nil is returned if there is no + // computed size available. + ComputedSize() *SizeEstimate +} + +// astNode implements the AstNode interface. +type astNode struct { + path []string + t *types.Type + expr ast.Expr + derivedSize *SizeEstimate +} + +// Path returns the field path to the node, if known. +func (e astNode) Path() []string { + return e.path +} + +// Type returns the type of the node. +func (e astNode) Type() *types.Type { + return e.t +} + +// Expr returns the underlying CEL expression. +func (e astNode) Expr() ast.Expr { + return e.expr +} + +// ComputedSize returns the precomputed or derived size estimate of the node. +func (e astNode) ComputedSize() *SizeEstimate { + return e.derivedSize +} + +// NewAstNode returns an AstNode with the given expression, path, type, and derived size. +func NewAstNode(expr ast.Expr, path []string, t *types.Type, derivedSize *SizeEstimate) AstNode { + return astNode{ + path: path, + t: t, + expr: expr, + derivedSize: derivedSize, + } +} + +// Option configures flags which affect cost computations. +type Option interface { + applyCoster(*coster) error +} + +type costerOption func(*coster) error + +func (opt costerOption) applyCoster(c *coster) error { + return opt(c) +} + +// CostOption is an alias for Option. +// +// Deprecated: use Option. +type CostOption = Option + +// PresenceTestHasCost determines whether presence testing has a cost of one or zero. +// +// Defaults to presence test has a cost of one. +func PresenceTestHasCost(hasCost bool) Option { + return costerOption(func(c *coster) error { + if hasCost { + c.presenceTestCost = selectAndIdentCost + return nil + } + c.presenceTestCost = FixedCostEstimate(0) + return nil + }) +} + +// FunctionEstimator provides a CallEstimate given the target and arguments for a specific function, overload pair. +type FunctionEstimator func(estimator Estimator, target *AstNode, args []AstNode) *CallEstimate + +// OverloadCostEstimate binds a FunctionEstimator to a specific function overload ID. +// +// When an OverloadCostEstimate is provided, it overrides the cost calculation of the Estimator +// provided to the Cost() call. +func OverloadCostEstimate(overloadID string, functionCoster FunctionEstimator) Option { + return costerOption(func(c *coster) error { + if functionCoster == nil { + return fmt.Errorf("OverloadCostEstimate(%q) requires a FunctionEstimator", overloadID) + } + c.overloadEstimators.mutable()[overloadID] = functionCoster + return nil + }) +} + +// Cost estimates the cost of the parsed and type checked CEL expression. +func Cost(checked *ast.AST, estimator Estimator, opts ...Option) (CostEstimate, error) { + c, err := newCoster(checked, estimator, opts...) + if err != nil { + return CostEstimate{}, err + } + return c.cost(checked.Expr()), nil +} + +// newCoster returns a coster configured by the options. Its configuration is fixed once it is +// returned; only the per-expression state gathered while costing changes. +func newCoster(checked *ast.AST, estimator Estimator, opts ...Option) (*coster, error) { + c := &coster{ + checkedAST: checked, + estimator: estimator, + modelRules: defaultModelRules(), + exprPaths: map[int64][]string{}, + localVars: make(scopes), + computedSizes: map[int64]SizeEstimate{}, + presenceTestCost: FixedCostEstimate(1), + } + for _, opt := range opts { + if mo, ok := opt.(ModelOption); ok { + c.modelRules.override(mo) + } + } + for _, opt := range opts { + if err := opt.applyCoster(c); err != nil { + return nil, err + } + } + // The package-level estimators are compiled under the default rules. + c.standardOverloadEstimators = stdOverloadEstimators + if !c.modelRules.isDefault() { + c.standardOverloadEstimators = StandardOverloadEstimatorsWithOptions(c.modelRules.modelOptions()...) + } + c.est = versionedEstimator{ + coster: c, + delegate: estimator, + version: c.modelVersion, + strategy: c.sizingStrategy, + } + c.evalCtx = estimatorEvalContext{ + coster: c, + estimator: estimator, + strategy: c.sizingStrategy, + version: c.modelVersion, + } + return c, nil +} + +// coster performs recursive cost and size calculation on checked AST expressions. +type coster struct { + // exprPaths maps from Expr Id to field path. + exprPaths map[int64][]string + // localVars tracks the local and iteration variables assigned during evaluation. + localVars scopes + // computedSizes tracks the computed sizes of call results. + computedSizes map[int64]SizeEstimate + + checkedAST *ast.AST + estimator Estimator + modelRules + overloadEstimators overloadTable[FunctionEstimator] + standardOverloadEstimators map[string]FunctionEstimator + // presenceTestCost will either be a zero or one based on whether has() macros count against cost computations. + presenceTestCost CostEstimate + // est and evalCtx are owned by the coster and handed out by pointer, so that passing them to + // estimators and sizing strategies does not allocate. + est versionedEstimator + evalCtx estimatorEvalContext +} + +// localVar captures the local variable size estimates if they exist for variables +type localVar struct { + exprID int64 + path []string + size *SizeEstimate +} + +// scopes is a stack of variable name to integer id stack to handle scopes created by cel.bind() like macros +type scopes map[string][]*localVar + +// push adds a variable name to the scope stack with its path and size estimates. +func (s scopes) push(varName string, expr ast.Expr, path []string, size *SizeEstimate) { + s[varName] = append(s[varName], &localVar{ + exprID: expr.ID(), + path: path, + size: size, + }) +} + +// pop removes the most recent variable from the scope stack for the given name. +func (s scopes) pop(varName string) { + varStack := s[varName] + s[varName] = varStack[:len(varStack)-1] +} + +// peek returns the top of the stack for the given variable name. +func (s scopes) peek(varName string) (*localVar, bool) { + varStack := s[varName] + if len(varStack) > 0 { + return varStack[len(varStack)-1], true + } + return nil, false +} + +// pushIterKey pushes the iteration key or index variable for a comprehension onto the scope stack. +func (c *coster) pushIterKey(varName string, rangeExpr ast.Expr) { + rangeSize := c.sizeOrUnknown(rangeExpr) + container := c.getType(rangeExpr).Kind() + var size *SizeEstimate + subpath := "@keys" + if container == types.ListKind { + s := FixedSizeEstimate(1) + size = &s + subpath = "@indices" + } else if rangeSize.Key != nil { + size = rangeSize.Key + } + var varPath []string + if path := c.getPath(rangeExpr); len(path) > 0 || c.modelVersion < 1 { + varPath = append(path, subpath) + } + c.localVars.push(varName, rangeExpr, varPath, size) +} + +// pushIterValue pushes the iteration value variable for a comprehension onto the scope stack. +func (c *coster) pushIterValue(varName string, rangeExpr ast.Expr) { + rangeSize := c.sizeOrUnknown(rangeExpr) + var size *SizeEstimate + if rangeSize.Elem != nil { + size = rangeSize.Elem + } + container := c.getType(rangeExpr).Kind() + subpath := "@values" + if container == types.ListKind { + subpath = "@items" + } + var varPath []string + if path := c.getPath(rangeExpr); len(path) > 0 || c.modelVersion < 1 { + varPath = append(path, subpath) + } + c.localVars.push(varName, rangeExpr, varPath, size) +} + +// pushIterSingle pushes a single iteration variable (items for list, keys for map) onto the scope stack. +func (c *coster) pushIterSingle(varName string, rangeExpr ast.Expr) { + rangeSize := c.sizeOrUnknown(rangeExpr) + var size *SizeEstimate + subpath := "@keys" + container := c.getType(rangeExpr).Kind() + if container == types.ListKind || (container == types.DynKind && rangeSize.Key == nil && rangeSize.Elem != nil) { + size = rangeSize.Elem + subpath = "@items" + } else { + size = rangeSize.Key + } + var varPath []string + if path := c.getPath(rangeExpr); len(path) > 0 || c.modelVersion < 1 { + varPath = append(path, subpath) + } + c.localVars.push(varName, rangeExpr, varPath, size) +} + +// pushLocalVar records a local variable binding with its path and size estimates. +func (c *coster) pushLocalVar(varName string, e ast.Expr) { + path := c.getPath(e) + c.localVars.push(varName, e, path, ownedSize(c.computeSize(e))) +} + +// peekLocalVar looks up the top of the scope stack for a local variable name. +func (c *coster) peekLocalVar(varName string) (*localVar, bool) { + return c.localVars.peek(varName) +} + +// popLocalVar pops the scope stack for a local variable name. +func (c *coster) popLocalVar(varName string) { + c.localVars.pop(varName) +} + +// cost recursively calculates the cost estimate for an expression node. +func (c *coster) cost(e ast.Expr) CostEstimate { + if e == nil { + return CostEstimate{} + } + switch e.Kind() { + case ast.LiteralKind: + return constCost + case ast.IdentKind: + return c.costIdent(e) + case ast.SelectKind: + return c.costSelect(e) + case ast.CallKind: + return c.costCall(e) + case ast.ListKind: + return c.costCreateList(e) + case ast.MapKind: + return c.costCreateMap(e) + case ast.StructKind: + return c.costCreateStruct(e) + case ast.ComprehensionKind: + if c.isBind(e) { + return c.costBind(e) + } + return c.costComprehension(e) + default: + return CostEstimate{} + } +} + +// costIdent estimates the cost of evaluating an identifier and tracks its path. +func (c *coster) costIdent(e ast.Expr) CostEstimate { + ident := e.AsIdent() + if v, ok := c.peekLocalVar(ident); ok { + c.addPath(e, v.path) + } else { + c.addPath(e, []string{ident}) + } + return selectAndIdentCost +} + +// costSelect estimates the cost of a field selection or map key access. +func (c *coster) costSelect(e ast.Expr) CostEstimate { + sel := e.AsSelect() + var sum CostEstimate + if sel.IsTestOnly() { + sum = sum.Add(c.presenceTestCost) + sum = sum.Add(branchQualifierCost(sel.Operand())) + } else { + sum = sum.Add(selectAndIdentCost) + } + sum = sum.Add(c.cost(sel.Operand())) + sum = sum.Add(c.relativeAttributeCost(sel.Operand())) + targetPath := c.getPath(sel.Operand()) + if len(targetPath) > 0 { + c.addPath(e, append(targetPath, sel.FieldName())) + } + return sum +} + +// branchQualifierCost is the cost of the qualification a presence test applies to the branch of a +// conditional at the root of its operand's attribute chain. The interpreter charges it when the +// selected branch is computed. When the branch is an attribute, the estimate already accounts for +// it with the cost of the branch's root, which the interpreter resolves without charging. +func branchQualifierCost(operand ast.Expr) CostEstimate { + root := attributeChainRoot(operand) + if !isConditionalCall(root) { + return CostEstimate{} + } + args := root.AsCall().Args() + if len(args) != 3 { + return CostEstimate{} + } + branch := func(e ast.Expr) CostEstimate { + if !isAttributeChain(e) || (e.Kind() == ast.SelectKind && e.AsSelect().IsTestOnly()) { + return FixedCostEstimate(SelectAndIdentCost) + } + return branchQualifierCost(e) + } + return branch(args[1]).Union(branch(args[2])) +} + +// attributeChainRoot returns the expression an attribute chain starts from, past the selections +// and index operations which extend it. +func attributeChainRoot(e ast.Expr) ast.Expr { + for { + switch e.Kind() { + case ast.SelectKind: + if e.AsSelect().IsTestOnly() { + return e + } + e = e.AsSelect().Operand() + case ast.CallKind: + call := e.AsCall() + switch call.FunctionName() { + case operators.Index, operators.OptIndex, operators.OptSelect: + if len(call.Args()) > 0 { + e = call.Args()[0] + continue + } + } + return e + default: + return e + } + } +} + +// relativeAttributeCost is the cost of qualifying a value which was computed rather than named. +func (c *coster) relativeAttributeCost(operand ast.Expr) CostEstimate { + if c.modelVersion < 1 || isAttributeChain(operand) { + return CostEstimate{} + } + return FixedCostEstimate(SelectAndIdentCost) +} + +// isAttributeChain reports whether an expression is resolved as part of a single attribute +// during evaluation. A chain begins at an identifier, or at a ternary which selects between +// attributes, and is extended by field selections and index operations, including their +// optional variants. +// +// This predicate must mirror the planner's decision to wrap an operand in a relative +// attribute: any expression which plans to an interpretable attribute extends the chain and +// does not incur an additional attribute resolution cost. +func isAttributeChain(e ast.Expr) bool { + switch e.Kind() { + case ast.IdentKind, ast.SelectKind: + return true + case ast.CallKind: + switch e.AsCall().FunctionName() { + case operators.Index, operators.OptIndex, operators.OptSelect, operators.Conditional: + return true + } + } + return false +} + +// costCall estimates the cost of evaluating a function call expression. +func (c *coster) costCall(e ast.Expr) CostEstimate { + if e.AsCall().FunctionName() == "cel.@block" { + return c.costBlock(e) + } + // Dyn is just a way to disable type-checking, so return the cost of 1 with the cost of the argument + if dynEstimate := c.maybeUnwrapDynCall(e); dynEstimate != nil { + return *dynEstimate + } + + // Continue estimating the cost of all other calls. + call := e.AsCall() + args := call.Args() + var sum CostEstimate + + // Index-like operators qualify their first argument, which requires a relative attribute + // when the operand is a computed value rather than a named one. + switch call.FunctionName() { + case operators.Index, operators.OptIndex, operators.OptSelect: + if len(args) > 0 { + sum = sum.Add(c.relativeAttributeCost(args[0])) + } + } + + var argCostBuf [4]CostEstimate + var argCosts []CostEstimate + if len(args) <= len(argCostBuf) { + argCosts = argCostBuf[:len(args)] + } else { + argCosts = make([]CostEstimate, len(args)) + } + for i, arg := range args { + argCosts[i] = c.cost(arg) + } + if call.FunctionName() == operators.Conditional && len(args) == 3 && c.modelVersion >= 1 { + argCosts[1] = conditionalBranchCost(args[1], argCosts[1]) + argCosts[2] = conditionalBranchCost(args[2], argCosts[2]) + } + + overloadIDs := c.checkedAST.GetOverloadIDs(e.ID()) + if len(overloadIDs) == 0 { + var argCostSum CostEstimate + for _, a := range argCosts { + argCostSum = argCostSum.Add(a) + } + return FixedCostEstimate(1).Add(sum).Add(argCostSum) + } + if call.IsMemberFunction() { + sum = sum.Add(c.cost(call.Target())) + } + var argTypeBuf [4]AstNode + var argTypes []AstNode + var targetNode AstNode + var targetType *AstNode + if c.needsCallNodes(overloadIDs) { + if len(args) <= len(argTypeBuf) { + argTypes = argTypeBuf[:len(args)] + } else { + argTypes = make([]AstNode, len(args)) + } + for i, arg := range args { + argTypes[i] = c.newAstNode(arg) + } + if call.IsMemberFunction() { + targetNode = c.newAstNode(call.Target()) + targetType = &targetNode + } + } + // Pick a cost estimate range that covers all the overload cost estimation ranges + fnCost := CostEstimate{Min: uint64(math.MaxUint64), Max: 0} + var resultSize *SizeEstimate + for _, overload := range overloadIDs { + overloadCost := c.functionCost(e, call.FunctionName(), overload, targetType, argTypes, argCosts) + fnCost = fnCost.Union(overloadCost.CostEstimate) + overloadSize := overloadCost.ResultSize + optIndex := c.modelVersion >= 1 && isOptionalIndex(overload) + // build and track the field path for index operations + if field, ok := c.indexFieldPath(overload); ok && len(args) > 0 { + if targetPath := c.getPath(args[0]); len(targetPath) > 0 { + c.addPath(e, append(targetPath, field)) + } else if containerSize := c.indexedContainerSize(overload, args[0]); containerSize != nil && containerSize.Elem != nil { + elemSize := *containerSize.Elem + overloadSize = &elemSize + } + } + if overloadSize == nil { + if optIndex { + overloadSize = c.optionalValueSize(e) + } else { + overloadSize = c.computeSize(e) + } + } + if optIndex && overloadSize != nil { + wrapped := optionalSizeOf(*overloadSize) + overloadSize = &wrapped + } + resultSize = mergeSizeEstimatePtr(resultSize, overloadSize) + } + c.setSize(e, resultSize) + return sum.Add(fnCost) +} + +// isOptionalIndex reports whether an overload is an optional index operation, l[?i] or m[?k], which +// returns an optional holding the element it selects. +func isOptionalIndex(overloadID string) bool { + switch overloadID { + case overloads.OptIndexList, overloads.OptIndexMap, overloads.OptIndexOptionalList, overloads.OptIndexOptionalMap: + return true + } + return false +} + +// indexedContainerSize returns the size of the container an index operation selects from. From +// version 1 the operand of an optional index on an optional receiver is the optional, whose element +// is the container. +func (c *coster) indexedContainerSize(overloadID string, operand ast.Expr) *SizeEstimate { + size := c.computeSize(operand) + if size == nil || c.modelVersion < 1 { + return size + } + switch overloadID { + case overloads.OptIndexOptionalList, overloads.OptIndexOptionalMap: + return size.Elem + } + return size +} + +// optionalValueSize estimates the value an optional-typed expression holds from the size hints for +// its path and the value's type. +func (c *coster) optionalValueSize(e ast.Expr) *SizeEstimate { + t := c.getType(e) + if t.Kind() != types.OpaqueKind || len(t.Parameters()) != 1 { + return nil + } + node := astNode{path: c.getPath(e), t: t.Parameters()[0]} + if size, ok := c.sizingStrategy.EstimateSize(c.newEstimateContext(), node); ok { + return &size + } + return nil +} + +func (c *coster) needsCallNodes(overloadIDs []string) bool { + if c.estimator != nil { + return true + } + std := c.standardOverloadEstimators + for _, id := range overloadIDs { + if _, ok := c.overloadEstimators.fns[id]; ok { + return true + } + if _, ok := std[id]; ok { + return true + } + } + return false +} + +// indexFieldPath reports the field path segment an index overload qualifies its operand with, so +// that the size hint recorded for a container's elements is found when one of them is read. +// +// The optional forms count only from version 1. Before the revision `l[?0]` recorded no path, +// so the `@items` hint that `l[0]` finds was never consulted and every estimate below an optional +// index was unbounded. A pinned caller keeps that. +func (c *coster) indexFieldPath(overloadID string) (string, bool) { + switch overloadID { + case overloads.IndexList: + return "@items", true + case overloads.IndexMap: + return "@values", true + case overloads.OptIndexList, overloads.OptIndexOptionalList: + return "@items", c.modelVersion >= 1 + case overloads.OptIndexMap, overloads.OptIndexOptionalMap: + return "@values", c.modelVersion >= 1 + } + return "", false +} + +// maybeUnwrapDynCall handles the 'dyn' call wrapper, returning an estimate if matched. +func (c *coster) maybeUnwrapDynCall(e ast.Expr) *CostEstimate { + call := e.AsCall() + if call.FunctionName() != "dyn" { + return nil + } + arg := call.Args()[0] + argCost := c.cost(arg) + c.copySizeEstimates(e, arg) + callCost := FixedCostEstimate(1).Add(argCost) + return &callCost +} + +// costBlock estimates the cost of a cel.@block([slot0, slot1, ...], resultExpr) call, +// binding each slot expression to @index0, @index1, ... while evaluating the block result. +func (c *coster) costBlock(e ast.Expr) CostEstimate { + call := e.AsCall() + args := call.Args() + if len(args) != 2 || args[0].Kind() != ast.ListKind { + return FixedCostEstimate(1) + } + slots := args[0].AsList().Elements() + var sum CostEstimate + for i, slot := range slots { + sum = sum.Add(c.cost(slot)) + c.pushLocalVar("@index"+strconv.Itoa(i), slot) + } + sum = sum.Add(c.cost(args[1])) + if p := c.getPath(args[1]); len(p) > 0 { + c.addPath(e, p) + } + c.copySizeEstimates(e, args[1]) + for i := len(slots) - 1; i >= 0; i-- { + c.popLocalVar("@index" + strconv.Itoa(i)) + } + return sum +} + +// costCreateList estimates the cost of constructing a list literal. +func (c *coster) costCreateList(e ast.Expr) CostEstimate { + create := e.AsList() + var sum CostEstimate + for _, elem := range create.Elements() { + sum = sum.Add(c.cost(elem)) + } + return sum.Add(createListBaseCost) +} + +// costCreateMap estimates the cost of constructing a map literal. +func (c *coster) costCreateMap(e ast.Expr) CostEstimate { + mapVal := e.AsMap() + var sum CostEstimate + for _, ent := range mapVal.Entries() { + entry := ent.AsMapEntry() + sum = sum.Add(c.cost(entry.Key())) + sum = sum.Add(c.cost(entry.Value())) + } + return sum.Add(createMapBaseCost) +} + +// costCreateStruct estimates the cost of constructing a struct or message literal. +func (c *coster) costCreateStruct(e ast.Expr) CostEstimate { + msgVal := e.AsStruct() + var sum CostEstimate + for _, ent := range msgVal.Fields() { + field := ent.AsStructField() + sum = sum.Add(c.cost(field.Value())) + } + return sum.Add(createMessageBaseCost) +} + +// costComprehension estimates the cost of evaluating a comprehension loop. +func (c *coster) costComprehension(e ast.Expr) CostEstimate { + comp := e.AsComprehension() + var sum CostEstimate + sum = sum.Add(c.cost(comp.IterRange())) + sum = sum.Add(c.cost(comp.AccuInit())) + c.pushLocalVar(comp.AccuVar(), comp.AccuInit()) + + // Track the iterRange of each IterVar and AccuVar for field path construction + if comp.HasIterVar2() { + c.pushIterKey(comp.IterVar(), comp.IterRange()) + c.pushIterValue(comp.IterVar2(), comp.IterRange()) + } else { + c.pushIterSingle(comp.IterVar(), comp.IterRange()) + } + + // Determine the cost for each element in the loop + loopCost := c.cost(comp.LoopCondition()) + stepCost := c.cost(comp.LoopStep()) + stepSize := c.computeSize(comp.LoopStep()) + + // Clear the intermediate variable tracking. + c.popLocalVar(comp.IterVar()) + if comp.HasIterVar2() { + c.popLocalVar(comp.IterVar2()) + } + + // Determine the result cost. + sum = sum.Add(c.cost(comp.Result())) + c.localVars.pop(comp.AccuVar()) + + // Estimate the cost of the loop. + rangeSize := c.sizeOrUnknown(comp.IterRange()) + // The loop runs once per element or entry of the range, which the sizing strategy may + // size by content rather than by length. + rangeCnt := countBounds(c.sizingStrategy, rangeSize) + rangeCnt.Key, rangeCnt.Elem = rangeSize.Key, rangeSize.Elem + rangeCost := rangeCnt.MultiplyByCost(stepCost.Add(loopCost)) + sum = sum.Add(rangeCost) + + switch k := comp.AccuInit().Kind(); k { + case ast.LiteralKind: + c.setSize(e, c.computeSize(comp.AccuInit())) + case ast.ListKind, ast.MapKind: + accuSize := rangeCnt + if k == ast.ListKind { + accuSize.Key = nil + } + if stepSize != nil { + if stepSize.Elem != nil { + accuSize.Elem = stepSize.Elem + } + if k == ast.MapKind && stepSize.Key != nil { + accuSize.Key = stepSize.Key + } + } + // A conditional step, as in filter, may skip an iteration, so the container may hold + // no entries at all. + if isConditionalCall(comp.LoopStep()) { + accuSize.Min = 0 + } + // The accumulated container holds at most one entry per iteration; report the size the + // sizing strategy assigns to it. + sized := containerSizeFor(c.sizingStrategy, accuSize, accumulatedEntrySize(accuSize)) + sized.Key, sized.Elem = accuSize.Key, accuSize.Elem + accuSize = sized + c.setSize(e, &accuSize) + } + return sum +} + +// accumulatedEntrySize returns the size of one entry of an accumulated container: its element, or +// its key and value. An unknown part is unbounded. +func accumulatedEntrySize(sz SizeEstimate) SizeEstimate { + entry := UnknownSizeEstimate() + if sz.Elem != nil { + entry = RangedSizeEstimate(sz.Elem.Min, sz.Elem.Max) + } + if sz.Key != nil { + withKey := entry.Add(RangedSizeEstimate(sz.Key.Min, sz.Key.Max)) + entry = RangedSizeEstimate(withKey.Min, withKey.Max) + } + return entry +} + +// conditionalBranchCost is the cost of a conditional's branch from cost model version 1. +// +// A branch which is an attribute chain rooted at an identifier, such as x, m.a or x[0], is resolved +// as part of the conditional's attribute: the interpreter charges the condition and the branch's +// qualifiers, but not the resolution of the branch's root identifier, which the estimate of the +// branch includes. A branch rooted at a nested conditional is priced by that conditional, and a +// computed branch is charged in full. +func conditionalBranchCost(branch ast.Expr, cost CostEstimate) CostEstimate { + if !isAttributeChain(branch) || attributeChainRoot(branch).Kind() != ast.IdentKind { + return cost + } + if branch.Kind() == ast.SelectKind && branch.AsSelect().IsTestOnly() { + return cost + } + return CostEstimate{ + Min: SafeSubtract(cost.Min, SelectAndIdentCost), + Max: SafeSubtract(cost.Max, SelectAndIdentCost), + } +} + +// isConditionalCall reports whether e is a call to the conditional operator. +func isConditionalCall(e ast.Expr) bool { + return e.Kind() == ast.CallKind && e.AsCall().FunctionName() == operators.Conditional +} + +// isBind returns true if the given expression represents a cel.bind() macro structure. +func (c *coster) isBind(e ast.Expr) bool { + comp := e.AsComprehension() + iterRange := comp.IterRange() + loopCond := comp.LoopCondition() + return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 && + loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral() == types.False && + !isAccumulatorVar(comp.AccuVar()) +} + +// costBind estimates the cost of a cel.bind() variable declaration macro. +func (c *coster) costBind(e ast.Expr) CostEstimate { + comp := e.AsComprehension() + var sum CostEstimate + // Binds are lazily initialized, so we retain the cost of an empty iteration range. + sum = sum.Add(c.cost(comp.IterRange())) + initCost := c.cost(comp.AccuInit()) + if c.modelVersion >= 1 && !mustReadVar(comp.Result(), comp.AccuVar()) { + // The bound value is computed only if the result reads it, so unless every evaluation of + // the result reads it, its cost contributes to the upper bound alone. + initCost = CostEstimate{Min: 0, Max: initCost.Max} + } + sum = sum.Add(initCost) + + c.pushLocalVar(comp.AccuVar(), comp.AccuInit()) + sum = sum.Add(c.cost(comp.Result())) + if p := c.getPath(comp.Result()); len(p) > 0 { + c.addPath(e, p) + } + // Associate the bind output size with the result size before popping the bound variable. + c.copySizeEstimates(e, comp.Result()) + c.popLocalVar(comp.AccuVar()) + + return sum +} + +// mustReadVar reports whether every evaluation of e reads the variable name. It is conservative: +// operands which may be skipped, such as the right side of a logical operator, the branches of a +// conditional, the alternative of an optional, and the body of a comprehension, do not count, +// and neither does the bound value of a nested bind, which is itself lazy. +func mustReadVar(e ast.Expr, name string) bool { + switch e.Kind() { + case ast.IdentKind: + return e.AsIdent() == name + case ast.SelectKind: + return mustReadVar(e.AsSelect().Operand(), name) + case ast.CallKind: + call := e.AsCall() + args := call.Args() + switch call.FunctionName() { + case operators.LogicalAnd, operators.LogicalOr, operators.Conditional: + return len(args) > 0 && mustReadVar(args[0], name) + case "or", "orValue": + if call.IsMemberFunction() { + return mustReadVar(call.Target(), name) + } + } + if call.IsMemberFunction() && mustReadVar(call.Target(), name) { + return true + } + for _, arg := range args { + if mustReadVar(arg, name) { + return true + } + } + return false + case ast.ListKind: + for _, elem := range e.AsList().Elements() { + if mustReadVar(elem, name) { + return true + } + } + return false + case ast.MapKind: + for _, entry := range e.AsMap().Entries() { + me := entry.AsMapEntry() + if mustReadVar(me.Key(), name) || mustReadVar(me.Value(), name) { + return true + } + } + return false + case ast.StructKind: + for _, field := range e.AsStruct().Fields() { + if mustReadVar(field.AsStructField().Value(), name) { + return true + } + } + return false + case ast.ComprehensionKind: + comp := e.AsComprehension() + if mustReadVar(comp.IterRange(), name) { + return true + } + // A bind's value is lazy: it is computed only when its result reads it, and its result, + // unless the bind shadows name, may read name directly. A loop's initial accumulator is + // always computed. + isBind := comp.IterRange().Kind() == ast.ListKind && comp.IterRange().AsList().Size() == 0 && + !isAccumulatorVar(comp.AccuVar()) + if isBind { + if comp.AccuVar() != name && mustReadVar(comp.Result(), name) { + return true + } + return mustReadVar(comp.Result(), comp.AccuVar()) && mustReadVar(comp.AccuInit(), name) + } + return mustReadVar(comp.AccuInit(), name) + default: + return false + } +} + +// calculateArgCost combines the costs of a call's arguments at the given cost model version. +func calculateArgCost(overloadID string, argCosts []CostEstimate, version uint32) CostEstimate { + switch overloadID { + case overloads.LogicalOr, overloads.LogicalAnd: + if len(argCosts) == 2 { + return CostEstimate{Min: argCosts[0].Min, Max: argCosts[0].Add(argCosts[1]).Max} + } + case overloads.Conditional: + if len(argCosts) == 3 { + return argCosts[0].Add(argCosts[1].Union(argCosts[2])) + } + case overloads.OptionalOrOptional, overloads.OptionalOrValueValue: + // The alternative is evaluated only when the receiver is empty, so it contributes to the + // upper bound alone. These are member overloads, which means the receiver is costed by + // the caller and argCosts holds only the alternative. Before version 1 the alternative + // was summed like any other argument. + if len(argCosts) == 1 && version >= 1 { + return CostEstimate{Min: 0, Max: argCosts[0].Max} + } + } + var sum CostEstimate + for _, a := range argCosts { + sum = sum.Add(a) + } + return sum +} + +func (c *coster) functionCost(e ast.Expr, function, overloadID string, target *AstNode, args []AstNode, argCosts []CostEstimate) CallEstimate { + argCost := calculateArgCost(overloadID, argCosts, c.modelVersion) + c.est.callID = e.ID() + estCtx := c.getEstimator() + if estimator, found := c.overloadEstimators.fns[overloadID]; found { + if est := estimator(estCtx, target, args); est != nil { + return CallEstimate{CostEstimate: est.Add(argCost), ResultSize: est.ResultSize} + } + } + if c.estimator != nil { + if est := c.estimator.EstimateCallCost(function, overloadID, target, args); est != nil { + return CallEstimate{CostEstimate: est.Add(argCost), ResultSize: est.ResultSize} + } + } + if estimator, found := c.standardOverloadEstimators[overloadID]; found { + if est := estimator(estCtx, target, args); est != nil { + return CallEstimate{CostEstimate: est.Add(argCost), ResultSize: est.ResultSize} + } + } + // O(1) functions + // See CostTracker.costCall for more details about O(1) cost calculations + + // Benchmarks suggest that most of the other operations take +/- 50% of a base cost unit + // which on an Intel xeon 2.20GHz CPU is 50ns. + return CallEstimate{CostEstimate: FixedCostEstimate(1).Add(argCost)} +} + +// getType returns the deduced type of an expression ID from the checked AST. +func (c *coster) getType(e ast.Expr) *types.Type { + return c.checkedAST.GetType(e.ID()) +} + +// getPath returns the tracked field path for an expression node, resolving through local variables if needed. +func (c *coster) getPath(e ast.Expr) []string { + if e == nil { + return nil + } + if e.Kind() == ast.IdentKind { + if v, found := c.peekLocalVar(e.AsIdent()); found { + return v.path[:] + } + } + if path, ok := c.exprPaths[e.ID()]; ok { + return path + } + return nil +} + +// addPath associates an expression ID with its path. +func (c *coster) addPath(e ast.Expr, path []string) { + c.exprPaths[e.ID()] = path +} + +var ( + stdOverloadEstimators = StandardOverloadEstimators() +) + +func isAccumulatorVar(name string) bool { + return name == accumulatorName || name == hiddenAccumulatorName +} + +func (c *coster) newAstNode(e ast.Expr) AstNode { + return astNode{ + path: c.getPath(e), + t: c.getType(e), + expr: e, + derivedSize: ownedSize(c.computeSize(e))} +} + +// ownedSize returns a size estimate pointer which the caller may retain or modify. +// +// computeSize returns the shared unitSizeEstimate and zeroSizeEstimate for scalars and empty or +// single-unit literals so that the common case does not allocate. Those values back every such +// estimate, so a pointer which escapes into a retained structure, or to a FunctionEstimator through +// AstNode.ComputedSize, is copied first; a write through it must never reach the shared values. +func ownedSize(sz *SizeEstimate) *SizeEstimate { + if sz == &unitSizeEstimate || sz == &zeroSizeEstimate { + owned := *sz + return &owned + } + return sz +} + +// setSize stores a computed size estimate for an expression ID. +func (c *coster) setSize(e ast.Expr, size *SizeEstimate) { + if size == nil { + return + } + // Store the computed size with the expression + c.computedSizes[e.ID()] = *size +} + +func (c *coster) sizeOrUnknown(node ast.Expr) SizeEstimate { + if sz := c.computeSize(node); sz != nil { + return *sz + } + return UnknownSizeEstimate() +} + +// copySizeEstimates copies computed sizes and entry sizes from a source expression to a destination expression. +func (c *coster) copySizeEstimates(dst, src ast.Expr) { + c.setSize(dst, c.computeSize(src)) +} + +type versionedEstimator struct { + coster *coster + delegate Estimator + version uint32 + strategy SizingStrategy + // callID is the expression ID of the call being estimated, if any. + callID int64 +} + +func (v *versionedEstimator) EstimateSize(element AstNode) *SizeEstimate { + if v.delegate == nil { + return nil + } + return v.delegate.EstimateSize(element) +} + +func (v *versionedEstimator) EstimateCallCost(function, overloadID string, target *AstNode, args []AstNode) *CallEstimate { + if v.delegate == nil { + return nil + } + return v.delegate.EstimateCallCost(function, overloadID, target, args) +} + +func (v *versionedEstimator) modelVersion() uint32 { + return v.version +} + +func (v *versionedEstimator) sizingStrategy() SizingStrategy { + return v.strategy +} + +func (v *versionedEstimator) resultType() (*types.Type, bool) { + return v.coster.checkedAST.GetType(v.callID), true +} + +func (c *coster) getEstimator() Estimator { + return &c.est +} + +func (c *coster) newEstimateContext() *estimatorEvalContext { + return &c.evalCtx +} + +// computeSize resolves the size estimate for an expression, caching the result when found. +func (c *coster) computeSize(e ast.Expr) *SizeEstimate { + if size, ok := c.computedSizes[e.ID()]; ok { + if size == unitSizeEstimate { + return &unitSizeEstimate + } + if size == zeroSizeEstimate { + return &zeroSizeEstimate + } + return &size + } + if e.Kind() == ast.IdentKind { + varName := e.AsIdent() + if v, ok := c.peekLocalVar(varName); ok && v.size != nil { + if c.modelVersion < 1 && c.estimator != nil { + node := astNode{expr: e, path: c.getPath(e), t: c.getType(e)} + if size := c.estimator.EstimateSize(node); size != nil { + c.computedSizes[e.ID()] = *size + return size + } + } + return v.size + } + } + strategy := c.sizingStrategy + if strategy == defaultSizing { + if size := computeExprSize(e); size != nil { + c.computedSizes[e.ID()] = *size + return size + } + // Without an Estimator to supply hints, only a container's size needs the strategy. + if t := c.getType(e); c.estimator == nil && !isContainerKind(t.Kind()) { + if size := computeTypeSize(t, c.modelVersion); size != nil { + c.computedSizes[e.ID()] = *size + return size + } + return nil + } + } + node := astNode{expr: e, path: c.getPath(e), t: c.getType(e)} + ctx := c.newEstimateContext() + if size, ok := strategy.EstimateSize(ctx, node); ok { + c.computedSizes[e.ID()] = size + if size == unitSizeEstimate { + return &unitSizeEstimate + } + if size == zeroSizeEstimate { + return &zeroSizeEstimate + } + return &size + } + if size := computeExprSize(e); size != nil { + return size + } + if size := computeTypeSize(c.getType(e), c.modelVersion); size != nil { + return size + } + return nil +} + +// computeExprSize computes the size estimate from literal and declared container expressions. +func computeExprSize(expr ast.Expr) *SizeEstimate { + var v uint64 + switch expr.Kind() { + case ast.LiteralKind: + switch ck := expr.AsLiteral().(type) { + case types.String: + // converting to runes here is an O(n) operation, but + // this is consistent with how size is computed at runtime, + // and how the language definition defines string size + v = uint64(len([]rune(ck))) + case types.Bytes: + v = uint64(len(ck)) + case types.Bool, types.Double, types.Duration, + types.Int, types.Timestamp, types.Uint, + types.Null: + return &unitSizeEstimate + default: + return nil + } + default: + return nil + } + if v == 1 { + return &unitSizeEstimate + } + if v == 0 { + return &zeroSizeEstimate + } + size := FixedSizeEstimate(v) + return &size +} + +// computeTypeSize returns a fixed unit size estimate if the type is a constant-size scalar. +// computeTypeSize returns the size every value of a type has at the given cost model version, or nil +// if values of the type vary in size. +// +// A scalar has size 1. An optional of a scalar has size 1 before version 1, and from version 1 is 1 +// plus the scalar, which is its element; see Optional. +func computeTypeSize(t *types.Type, version uint32) *SizeEstimate { + if isScalar(t) { + return &unitSizeEstimate + } + if isOptionalType(t) && isScalar(t.Parameters()[0]) { + if version < 1 { + return &unitSizeEstimate + } + size := optionalSizeOf(unitSizeEstimate) + return &size + } + return nil +} + +// isOptionalType reports whether t is an optional type with a value type. +func isOptionalType(t *types.Type) bool { + return t.Kind() == types.OpaqueKind && t.TypeName() == "optional_type" && len(t.Parameters()) == 1 +} + +// optionalSizeOf returns the size of an optional which holds a value of size v, from cost model +// version 1: 1 plus the value, whose size is the optional's element. See Optional. +func optionalSizeOf(v SizeEstimate) SizeEstimate { + size := RangedSizeEstimate(SafeAdd(1, v.Min), SafeAdd(1, v.Max)) + size.Elem = &v + return size +} + +// isScalar returns true if the given type is known to be of a constant size at +// compile time. isScalar will return false for strings (they are variable-width) +// in addition to protobuf.Any and protobuf.Value (their size is not knowable at compile time). +func isScalar(t *types.Type) bool { + switch t.Kind() { + case types.BoolKind, types.DoubleKind, types.DurationKind, types.IntKind, types.TimestampKind, types.UintKind, types.TypeKind: + return true + } + return false +} + +var ( + unknownSizeEstimate = SizeEstimate{Min: 0, Max: math.MaxUint64} + unknownCostEstimate = unknownSizeEstimate.MultiplyByCostFactor(1) + unitSizeEstimate = FixedSizeEstimate(1) + zeroSizeEstimate = FixedSizeEstimate(0) + + selectAndIdentCost = FixedCostEstimate(SelectAndIdentCost) + constCost = FixedCostEstimate(ConstCost) + + createListBaseCost = FixedCostEstimate(ListCreateBaseCost) + createMapBaseCost = FixedCostEstimate(MapCreateBaseCost) + createMessageBaseCost = FixedCostEstimate(StructCreateBaseCost) + + accumulatorName = "__result__" + hiddenAccumulatorName = "@result" +) diff --git a/vendor/cel.dev/cel-go/common/cost/helper.go b/vendor/cel.dev/cel-go/common/cost/helper.go new file mode 100644 index 00000000000..ba5d9564593 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/helper.go @@ -0,0 +1,599 @@ +// 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 cost + +import ( + "math" + "math/bits" + + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// ContainerSizing describes what the size of a container measures under a sizing strategy. +type ContainerSizing int + +const ( + // SizedByLength means a container's size is its length, as under the default sizing strategy. + SizedByLength ContainerSizing = iota + // SizedByContent means a container's size is 1 plus the sizes of its entries, as under the + // aggregate sizing strategy. + SizedByContent + // SizedOpaque means the relationship between a container's size and its contents is not known, + // as under a custom sizing strategy. + SizedOpaque +) + +// ContainerSizingOf reports what a container's size measures for the given Estimator. +// +// A hand-written FunctionEstimator which builds a result's size from lengths, rather than from +// the sizes of its operands, uses it to report the size in the units of the sizing strategy. For +// an Estimator which carries no strategy, sizes are lengths. Prefer describing an overload by an +// OverloadModel, whose List and Map follow the strategy themselves. +func ContainerSizingOf(estimator Estimator) ContainerSizing { + s, ok := estimator.(sizingStrategyContext) + if !ok { + return SizedByLength + } + switch s.sizingStrategy().(type) { + case defaultSizingStrategy: + return SizedByLength + case *aggregateSizingStrategy: + return SizedByContent + default: + return SizedOpaque + } +} + +// sizingStrategyContext is implemented by EstimateContext values which expose their sizing +// strategy. It is unexported because EstimateContext is an interface third parties may implement. +type sizingStrategyContext interface { + sizingStrategy() SizingStrategy +} + +// strategyOf returns the sizing strategy of a context. A context which does not report one, such +// as a third-party EstimateContext, is sized by the default strategy, whose sizes are lengths. +func strategyOf(ctx EstimateContext) SizingStrategy { + if s, ok := ctx.(sizingStrategyContext); ok { + return s.sizingStrategy() + } + return defaultSizing +} + +// resultTypeContext is implemented by EstimateContext values which know the checked type of the +// call's result. It is unexported because EstimateContext is an interface third parties may +// implement. +type resultTypeContext interface { + resultType() (*types.Type, bool) +} + +// operandKind returns the kind of the checked type of an Arg, Target or Result operand. +func operandKind(ctx EstimateContext, e QuantityExpr) (types.Kind, bool) { + var t *types.Type + var ok bool + switch x := e.(type) { + case argExpr: + t, ok = ctx.ArgType(x.index) + case targetExpr: + t, ok = ctx.TargetType() + case resultExpr: + if rc, isRC := ctx.(resultTypeContext); isRC { + t, ok = rc.resultType() + } + } + if !ok || t == nil { + return types.UnspecifiedKind, false + } + switch t.Kind() { + case types.DynKind, types.AnyKind, types.TypeParamKind, types.UnspecifiedKind, types.ErrorKind: + return types.UnspecifiedKind, false + } + return t.Kind(), true +} + +// operandNodeContext is implemented by EstimateContext values which know the AST nodes of the +// call being estimated. It is unexported because EstimateContext is an interface third parties may +// implement. +type operandNodeContext interface { + operandNode(e QuantityExpr) (AstNode, bool) +} + +// operandLiteral returns the value of an Arg or Target operand which is a literal. +func operandLiteral(ctx EstimateContext, e QuantityExpr) (ref.Val, bool) { + oc, ok := ctx.(operandNodeContext) + if !ok { + return nil, false + } + node, ok := oc.operandNode(e) + if !ok || node == nil || node.Expr() == nil || node.Expr().Kind() != ast.LiteralKind { + return nil, false + } + return node.Expr().AsLiteral(), true +} + +// hasUnitLength reports whether every value of a kind has length 1: those which are neither +// strings, bytes nor containers. +func hasUnitLength(k types.Kind) bool { + switch k { + case types.BoolKind, types.DoubleKind, types.DurationKind, types.IntKind, types.NullTypeKind, + types.TimestampKind, types.UintKind: + return true + default: + return false + } +} + +// totalEstimate bounds the summed size of one part of a container, its elements or its keys, from +// the container's size estimate sz and the estimated size of one part. whole reports whether the +// part is the container's entire content, as the elements of a list are, rather than a share of +// it, as the keys or values of a map are. +// +// - Default strategy: a container's size is its length, so the total is the length multiplied +// by the part size. +// - Aggregate strategy: a container's size is 1 plus the sizes of its entries, so every part +// totals at most the content size, sz-1. The content size is also the lower bound when the +// part is the whole content; otherwise the lower bound is 0. +// - Other strategies define no relationship between a container's size and its contents, so the +// total is unknown. +// +// A value which the checker typed as having no elements, such as a string, and a container which +// may not hold anything, total 0. A container which may hold something, but whose part size is not +// stated, has an unknown total under the default strategy. +func totalEstimate(ctx EstimateContext, operand QuantityExpr, sz SizeEstimate, part *SizeEstimate, whole bool) SizeEstimate { + if !carriesElements(ctx, operand) || !mayHoldElements(ctx, sz) { + return FixedSizeEstimate(0) + } + switch strategyOf(ctx).(type) { + case defaultSizingStrategy: + if part == nil { + return UnknownSizeEstimate() + } + total := RangedSizeEstimate(sz.Min, sz.Max).Multiply(*part) + return RangedSizeEstimate(total.Min, total.Max) + case *aggregateSizingStrategy: + hi := SafeSubtract(sz.Max, 1) + if sz.Max == math.MaxUint64 { + hi = math.MaxUint64 + } + lo := uint64(0) + if whole { + lo = SafeSubtract(sz.Min, 1) + } + return RangedSizeEstimate(min(lo, hi), hi) + default: + return UnknownSizeEstimate() + } +} + +// isListOperand reports whether the operand of a total is a list, whose elements are its entire +// content, rather than a map, whose values are only part of it. The checked type decides when it is +// known; otherwise the metadata does, since a list states an element size but no key size. +func isListOperand(ctx EstimateContext, operand QuantityExpr, sz SizeEstimate) bool { + if kind, known := operandKind(ctx, operand); known { + return kind == types.ListKind + } + return sz.Elem != nil && sz.Key == nil +} + +// countBounds bounds the element count of a container from its size estimate under a strategy. +// It bounds comprehension iteration counts; see costComprehension. +// +// The default strategy sizes a container by its length, so the count is the size itself. The +// aggregate strategy sizes a container as 1 plus the sizes of its entries, each of which is at +// least 1, so the count is at least the content size divided by the largest entry. Its upper bound +// is the size itself rather than the content size divided by the smallest entry, so the bound +// remains sound for a size which a custom estimator reports as a length. Other strategies make no +// guarantee, so the count is unknown. +func countBounds(strategy SizingStrategy, sz SizeEstimate) SizeEstimate { + switch strategy.(type) { + case defaultSizingStrategy: + return RangedSizeEstimate(sz.Min, sz.Max) + case *aggregateSizingStrategy: + entry := aggregateEntrySize(sz) + lo := uint64(0) + if sz.Min > 1 && entry.Max != math.MaxUint64 { + content := sz.Min - 1 + lo = content / entry.Max + if content%entry.Max != 0 { + lo++ + } + } + return RangedSizeEstimate(min(lo, sz.Max), sz.Max) + default: + return UnknownSizeEstimate() + } +} + +// aggregateEntrySize bounds the aggregate size of one entry of a container: an element of a list, +// or a key and value of a map. Every value has aggregate size at least 1. +func aggregateEntrySize(sz SizeEstimate) SizeEstimate { + elem := RangedSizeEstimate(1, math.MaxUint64) + if sz.Elem != nil { + elem = AtLeastOneSize(RangedSizeEstimate(sz.Elem.Min, sz.Elem.Max)) + } + if sz.Key == nil { + return elem + } + key := AtLeastOneSize(RangedSizeEstimate(sz.Key.Min, sz.Key.Max)) + sum := key.Add(elem) + return RangedSizeEstimate(sum.Min, sum.Max) +} + +// rational is a non-negative scale factor p/q. A zero q marks a factor with no exact rational form +// within the supported range, which is applied in floating point. +type rational struct { + p, q uint64 +} + +// maxRationalDenominator bounds the denominator of an exact scale factor so that products of +// factors, and of a factor with a quantity, can be computed in 128-bit arithmetic. +const maxRationalDenominator = uint64(1) << 32 + +// rationalOf returns the simplest rational p/q whose float64 value is f, found from the continued +// fraction expansion of f. A decimal factor such as 0.1 is therefore applied as exactly 1/10, which +// is what the author wrote, rather than as the binary value nearest to it. +// +// Factors which are not finite and positive, or whose simplest form needs a denominator above +// maxRationalDenominator, report q == 0. +func rationalOf(f float64) rational { + if math.IsNaN(f) || math.IsInf(f, 0) || f <= 0 || f >= math.MaxUint64 { + return rational{} + } + // Convergents h/k of the continued fraction of f. + hPrev, h := uint64(0), uint64(1) + kPrev, k := uint64(1), uint64(0) + x := f + for i := 0; i < 64; i++ { + a := math.Floor(x) + if a >= math.MaxUint64 { + return rational{} + } + ai := uint64(a) + hiH, loH := bits.Mul64(ai, h) + hiK, loK := bits.Mul64(ai, k) + if hiH != 0 || hiK != 0 { + return rational{} + } + hNext, carryH := bits.Add64(loH, hPrev, 0) + kNext, carryK := bits.Add64(loK, kPrev, 0) + if carryH != 0 || carryK != 0 || kNext > maxRationalDenominator { + return rational{} + } + hPrev, h = h, hNext + kPrev, k = k, kNext + if float64(h)/float64(k) == f { + return rational{p: h, q: k} + } + frac := x - a + if frac == 0 { + return rational{} + } + x = 1 / frac + } + return rational{} +} + +// mul returns the product of two rationals in lowest terms, and false if the product does not fit +// within the supported range. +func (r rational) mul(o rational) (rational, bool) { + if r.q == 0 || o.q == 0 { + return rational{}, false + } + // Cross-reduce before multiplying to keep the terms small. + g1, g2 := gcd(r.p, o.q), gcd(o.p, r.q) + p1, q2 := r.p/g1, o.q/g1 + p2, q1 := o.p/g2, r.q/g2 + hiP, p := bits.Mul64(p1, p2) + hiQ, q := bits.Mul64(q1, q2) + if hiP != 0 || hiQ != 0 || q > maxRationalDenominator { + return rational{}, false + } + return rational{p: p, q: q}, true +} + +func gcd(a, b uint64) uint64 { + for b != 0 { + a, b = b, a%b + } + if a == 0 { + return 1 + } + return a +} + +// scale computes ⌈x · f⌉: +// +// - the factor is applied as its exact rational form when it has one (see rationalOf), so the +// result is exact for every input, including those above 2^53; +// - an unbounded input stays unbounded for any positive factor; +// - a zero input, or a zero or negative factor, yields zero; a NaN factor yields ⊤, so that an +// invalid factor cannot make a quantity free. +func scale(x uint64, f float64, r rational) uint64 { + switch { + case math.IsNaN(f): + return math.MaxUint64 + case f <= 0 || x == 0: + return 0 + case x == math.MaxUint64 || math.IsInf(f, 1): + return math.MaxUint64 + case r.q == 0: + return SafeMultiplyByFactor(x, f) + } + hi, lo := bits.Mul64(x, r.p) + if hi >= r.q { + return math.MaxUint64 + } + quo, rem := bits.Div64(hi, lo, r.q) + if rem != 0 { + if quo == math.MaxUint64 { + return math.MaxUint64 + } + quo++ + } + return quo +} + +// normalize moves scale factors outward so that each product and each uniformly scaled sum is +// rounded once: +// +// Scale(Scale(a, f), g) -> Scale(a, f·g) +// Mul(Scale(a, f), Scale(b, g), c) -> Scale(Mul(a, b, c), f·g) +// Sum(Scale(a, f), Scale(b, f)) -> Scale(Sum(a, b), f) +// +// Factors are combined as exact rationals. A combination which has no exact form within the +// supported range is left in place. +func normalize(e QuantityExpr) QuantityExpr { + if e == nil { + return nil + } + switch x := e.(type) { + case scaleExpr: + inner := normalize(x.expr) + if s, ok := inner.(scaleExpr); ok { + if r, ok := x.rat.mul(s.rat); ok { + return scaleExpr{expr: s.expr, factor: x.factor * s.factor, rat: r} + } + } + return scaleExpr{expr: inner, factor: x.factor, rat: x.rat} + case mulExpr: + terms := make([]QuantityExpr, 0, len(x.terms)) + for _, t := range x.terms { + t = normalize(t) + if m, ok := t.(mulExpr); ok { + terms = append(terms, m.terms...) + continue + } + terms = append(terms, t) + } + factor, rat, hoisted := 1.0, rational{p: 1, q: 1}, false + plain := make([]QuantityExpr, 0, len(terms)) + for _, t := range terms { + if s, ok := t.(scaleExpr); ok { + if r, ok := rat.mul(s.rat); ok { + factor, rat, hoisted = factor*s.factor, r, true + plain = append(plain, s.expr) + continue + } + } + plain = append(plain, t) + } + if hoisted { + return scaleExpr{expr: mulExpr{terms: plain}, factor: factor, rat: rat} + } + return mulExpr{terms: plain} + case addExpr: + terms := make([]QuantityExpr, len(x.terms)) + for i, t := range x.terms { + terms[i] = normalize(t) + } + if len(terms) > 1 { + if first, ok := terms[0].(scaleExpr); ok && first.rat.q != 0 { + inner := make([]QuantityExpr, len(terms)) + same := true + for i, t := range terms { + s, ok := t.(scaleExpr) + if !ok || s.rat != first.rat { + same = false + break + } + inner[i] = s.expr + } + if same { + return scaleExpr{expr: addExpr{terms: inner}, factor: first.factor, rat: first.rat} + } + } + } + return addExpr{terms: terms} + case subExpr: + return subExpr{lhs: normalize(x.lhs), rhs: normalize(x.rhs)} + case minExpr: + return minExpr{lhs: normalize(x.lhs), rhs: normalize(x.rhs)} + case maxExpr: + return maxExpr{lhs: normalize(x.lhs), rhs: normalize(x.rhs)} + case unionExpr: + terms := make([]QuantityExpr, len(x.terms)) + for i, t := range x.terms { + terms[i] = normalize(t) + } + return unionExpr{terms: terms} + case rangedExpr: + return rangedExpr{minExpr: normalize(x.minExpr), maxExpr: normalize(x.maxExpr)} + case atLeastOneExpr: + return atLeastOneExpr{expr: normalize(x.expr)} + case listExpr: + return listExpr{lenExpr: normalize(x.lenExpr), elemExpr: normalize(x.elemExpr)} + case mapExpr: + return mapExpr{sizeExpr: normalize(x.sizeExpr), keyExpr: normalize(x.keyExpr), valExpr: normalize(x.valExpr)} + case optionalExpr: + return optionalExpr{valueExpr: normalize(x.valueExpr)} + default: + // Atoms and projections contain no scale factors. + return e + } +} + +// carriesElements reports whether an expression may denote a container, and so whether an absent +// element or key size on its estimate means "unknown" rather than "none". +// +// Constants, integer values and totals are scalars. Arg, Target and Result may be containers, +// unless the checker typed the argument or receiver as something which has no elements, such as a +// string. The element and key projections may be containers, as may any combinator with such an +// operand. +func carriesElements(ctx EstimateContext, e QuantityExpr) bool { + switch x := e.(type) { + case argExpr: + t, ok := ctx.ArgType(x.index) + return !ok || mayBeContainerType(t) + case targetExpr: + t, ok := ctx.TargetType() + return !ok || mayBeContainerType(t) + case resultExpr, elemExpr, keyExpr, listExpr, mapExpr, optionalExpr: + return true + case subExpr: + return carriesElements(ctx, x.lhs) + case scaleExpr: + return carriesElements(ctx, x.expr) + case atLeastOneExpr: + return carriesElements(ctx, x.expr) + case minExpr: + return carriesElements(ctx, x.lhs) || carriesElements(ctx, x.rhs) + case maxExpr: + return carriesElements(ctx, x.lhs) || carriesElements(ctx, x.rhs) + case rangedExpr: + return carriesElements(ctx, x.minExpr) || carriesElements(ctx, x.maxExpr) + case addExpr: + return anyCarriesElements(ctx, x.terms) + case mulExpr: + return anyCarriesElements(ctx, x.terms) + case unionExpr: + return anyCarriesElements(ctx, x.terms) + default: + return false + } +} + +func anyCarriesElements(ctx EstimateContext, terms []QuantityExpr) bool { + for _, t := range terms { + if carriesElements(ctx, t) { + return true + } + } + return false +} + +// mayBeContainerType reports whether a value of the checked type may hold elements or keys. Only +// the kinds which cannot, such as strings, bytes and numbers, are excluded; an unknown type may be +// anything. +func mayBeContainerType(t *types.Type) bool { + if t == nil { + return true + } + k := t.Kind() + return !hasUnitLength(k) && k != types.StringKind && k != types.BytesKind +} + +// mergeMeta returns the union of the operands' key and element sizes. +// +// Metadata is attached only when some operand states it. When one does, an operand which may be a +// non-empty container but states no metadata contributes an unknown size, so the union cannot be +// narrower than what that operand may hold. Scalar operands, and empty containers, contribute +// nothing. +func mergeMeta(ctx EstimateContext, exprs []QuantityExpr, ests []SizeEstimate) (key, elem *SizeEstimate) { + anyKey, anyElem := false, false + for _, s := range ests { + anyKey = anyKey || s.Key != nil + anyElem = anyElem || s.Elem != nil + } + if !anyKey && !anyElem { + return nil, nil + } + for i, s := range ests { + // An operand which may be a non-empty container but states nothing about its contents may + // hold anything. One which states an element size but no key size is a list, which has no + // keys. + nonEmpty := mayHoldElements(ctx, s) + unstated := nonEmpty && s.Key == nil && s.Elem == nil && carriesElements(ctx, exprs[i]) + if anyElem { + e := s.Elem + if e == nil && (unstated || (s.Key != nil && nonEmpty)) { + e = &unknownSizeEstimate + } + elem = mergeSizeEstimatePtr(elem, e) + } + if anyKey { + k := s.Key + if k == nil && unstated { + k = &unknownSizeEstimate + } + key = mergeSizeEstimatePtr(key, k) + } + } + return key, elem +} + +// combineMeta attaches the operands' metadata, as mergeMeta merges it, to a combined estimate. +func combineMeta(ctx EstimateContext, size SizeEstimate, exprs []QuantityExpr, ests []SizeEstimate) SizeEstimate { + size.Key, size.Elem = mergeMeta(ctx, exprs, ests) + return size +} + +// mayHoldElements reports whether a container with the given size estimate may be non-empty under +// the context's sizing strategy. An empty container has size 0 under the default strategy and size +// 1 under the aggregate strategy. +func mayHoldElements(ctx EstimateContext, s SizeEstimate) bool { + if _, aggregate := strategyOf(ctx).(*aggregateSizingStrategy); aggregate { + return s.Max > 1 + } + return s.Max > 0 +} + +// aggregateResultSize raises the upper bound of a result size estimated under the aggregate +// strategy to at least 1. That strategy sizes an empty string, bytes value, list or map as 1, so a +// model whose result may be empty, such as a substring which drops the whole receiver, could +// otherwise report an upper bound of 0. Raising an upper bound is sound for any value. +func aggregateResultSize(strategy SizingStrategy, sz SizeEstimate) SizeEstimate { + if _, aggregate := strategy.(*aggregateSizingStrategy); aggregate && sz.Max == 0 { + sz.Max = 1 + } + return sz +} + +// containerSize returns the size the context's sizing strategy assigns to a container of n +// entries, each of the given size; see containerSizeFor. +func containerSize(ctx EstimateContext, n, entry SizeEstimate) SizeEstimate { + return containerSizeFor(strategyOf(ctx), n, entry) +} + +// containerSizeFor returns the size a strategy assigns to a container of n entries, each of the +// given size. +// +// The default strategy sizes a container by its length. The aggregate strategy sizes it as 1 plus +// the sizes of its entries, each of which is at least 1. Other strategies define no relationship, +// so the size is unknown. +func containerSizeFor(strategy SizingStrategy, n, entry SizeEstimate) SizeEstimate { + switch strategy.(type) { + case defaultSizingStrategy: + return RangedSizeEstimate(n.Min, n.Max) + case *aggregateSizingStrategy: + e := AtLeastOneSize(entry) + return RangedSizeEstimate( + SafeAdd(1, SafeMultiply(n.Min, e.Min)), + SafeAdd(1, SafeMultiply(n.Max, e.Max)), + ) + default: + return UnknownSizeEstimate() + } +} diff --git a/vendor/cel.dev/cel-go/common/cost/model.go b/vendor/cel.dev/cel-go/common/cost/model.go new file mode 100644 index 00000000000..6273c161572 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/model.go @@ -0,0 +1,1608 @@ +// 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 cost + +import ( + "math" + "slices" + "sync" + + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" +) + +// TypeContext provides type information for AST nodes. +type TypeContext interface { + // TargetType returns the deduced type of the receiver/target object, if present. + TargetType() (*types.Type, bool) + + // ArgType returns the deduced type of the argument at the given 0-based index. + ArgType(index int) (*types.Type, bool) +} + +// EstimateContext provides size and argument evaluation during cost estimation. +type EstimateContext interface { + TypeContext + + // Arg returns the size estimate for the argument at the given 0-based index. + Arg(index int) (SizeEstimate, bool) + + // Target returns the size estimate for the receiver/target object, if present. + Target() (SizeEstimate, bool) + + // Result returns the size estimate of the evaluated result, if known. + Result() (SizeEstimate, bool) + + // Estimator returns the user-provided Estimator, if configured. + Estimator() Estimator + + // Size returns the estimated size based on the SizingStrategy configured on the Estimator. + Size(node AstNode) SizeEstimate + + // ArgValue returns the literal/constant uint64 value of the argument at index, or defaultVal. + ArgValue(index int, defaultVal uint64) uint64 + + // TargetValue returns the literal/constant uint64 value of the receiver/target, or defaultVal. + TargetValue(defaultVal uint64) uint64 +} + +// TrackContext provides value size and argument evaluation during runtime cost tracking. +type TrackContext interface { + TypeContext + + // Arg returns the size of the argument at the given 0-based index. + Arg(index int) uint64 + + // Target returns the size of the receiver/target object, if present. + Target() uint64 + + // Result returns the size of the evaluated result, if present. + Result() uint64 + + // Estimator returns the runtime ActualCostEstimator, if configured. + Estimator() ActualCostEstimator + + // Size returns the actual runtime size of a value. + Size(value ref.Val) uint64 + + // ArgValue returns the uint64 value of the argument at index, or defaultVal. + ArgValue(index int, defaultVal uint64) uint64 + + // TargetValue returns the uint64 value of the receiver/target, or defaultVal. + TargetValue(defaultVal uint64) uint64 + + // ArgVal returns the value of the argument at the given 0-based index, if present. + ArgVal(index int) (ref.Val, bool) + + // TargetVal returns the value of the receiver/target object, if present. + TargetVal() (ref.Val, bool) + + // ResultVal returns the evaluated result value, if present. + ResultVal() (ref.Val, bool) +} + +// QuantityExpr represents a computable size or cost equation. +type QuantityExpr interface { + estimate(ctx EstimateContext) SizeEstimate + track(ctx TrackContext) uint64 +} + +// ModelVersionOf reports the cost model version the given Estimator is configured with, or +// math.MaxUint32 if the estimator does not carry version metadata. +func ModelVersionOf(estimator Estimator) uint32 { + if v, ok := estimator.(interface{ modelVersion() uint32 }); ok { + return v.modelVersion() + } + return defaultModelVersion +} + +// targetInspector is an internal interface for expressions that inspect receiver/target presence. +type targetInspector interface { + hasTarget() bool +} + +// hasTarget returns true if the expression or any of its subexpressions references the target object. +func hasTarget(expr QuantityExpr) bool { + if expr == nil { + return false + } + if r, ok := expr.(targetInspector); ok { + return r.hasTarget() + } + return false +} + +// valueSource is an internal interface for expressions which denote runtime values rather than +// only a quantity derived from them. +// +// Element and key projections read the value they project from, so they are defined only over the +// expressions implementing this interface: Arg, Target, Result, and the element and key +// projections ElemOf and KeyOf. An expression may denote more than one value, as the elements of +// a list do. +// +// OverloadModel.Validate reports a projection over any other expression, so a model which cannot +// answer a projection at runtime is rejected when it is registered rather than mispriced when it +// is evaluated. +type valueSource interface { + QuantityExpr + + // eachValue calls fn with each value the expression denotes in the given context. An + // expression which denotes no value in the context makes no calls. + eachValue(ctx TrackContext, fn func(ref.Val)) +} + +// eachValueOf calls fn with each value denoted by expr, and makes no calls if expr does not denote +// a value. +func eachValueOf(expr QuantityExpr, ctx TrackContext, fn func(ref.Val)) { + if src, ok := expr.(valueSource); ok { + src.eachValue(ctx, fn) + } +} + +// eachElement calls fn with each element of a list, each value of a map, or the value an optional +// holds. Values which are not containers have no elements. +func eachElement(val ref.Val, fn func(ref.Val)) { + switch v := val.(type) { + case *types.Optional: + if v.HasValue() { + fn(v.GetValue()) + } + case traits.Lister: + for it := v.Iterator(); it.HasNext() == types.True; { + fn(it.Next()) + } + case traits.Mapper: + for it := v.Iterator(); it.HasNext() == types.True; { + fn(v.Get(it.Next())) + } + } +} + +// eachKey calls fn with each key of a map. Values which are not maps have no keys. +func eachKey(val ref.Val, fn func(ref.Val)) { + if m, ok := val.(traits.Mapper); ok { + for it := m.Iterator(); it.HasNext() == types.True; { + fn(it.Next()) + } + } +} + +// constExpr represents a constant integer quantity. +type constExpr struct { + val uint64 +} + +func (c constExpr) estimate(_ EstimateContext) SizeEstimate { + return FixedSizeEstimate(c.val) +} + +func (c constExpr) track(_ TrackContext) uint64 { + return c.val +} + +func (constExpr) hasTarget() bool { return false } + +// Const creates a constant quantity expression. +func Const(val uint64) QuantityExpr { + return constExpr{val: val} +} + +// argExpr represents the size of a function argument. +type argExpr struct { + index int +} + +func (a argExpr) estimate(ctx EstimateContext) SizeEstimate { + if sz, ok := ctx.Arg(a.index); ok { + return sz + } + return UnknownSizeEstimate() +} + +func (a argExpr) track(ctx TrackContext) uint64 { + return ctx.Arg(a.index) +} + +func (a argExpr) eachValue(ctx TrackContext, fn func(ref.Val)) { + if val, ok := ctx.ArgVal(a.index); ok { + fn(val) + } +} + +func (argExpr) hasTarget() bool { return false } + +// Arg creates an expression referencing the size of the argument at the given index. +func Arg(index int) QuantityExpr { + return argExpr{index: index} +} + +// ArgElem creates an expression referencing the element size of the argument at the given index. +func ArgElem(index int) QuantityExpr { + return ElemOf(Arg(index)) +} + +// ArgKey creates an expression referencing the key size of the argument at the given index. +func ArgKey(index int) QuantityExpr { + return KeyOf(Arg(index)) +} + +// intArgExpr represents the integer value of an argument. +type intArgExpr struct { + index int + defaultVal uint64 +} + +func (a intArgExpr) estimate(ctx EstimateContext) SizeEstimate { + if versionOf(ctx) < 1 { + return FixedSizeEstimate(ctx.ArgValue(a.index, a.defaultVal)) + } + // A literal reports the same value for any default; a non-literal reports the default. + lo, hi := ctx.ArgValue(a.index, 0), ctx.ArgValue(a.index, math.MaxUint64) + if lo != hi { + return UnknownSizeEstimate() + } + return FixedSizeEstimate(lo) +} + +func (a intArgExpr) track(ctx TrackContext) uint64 { + return ctx.ArgValue(a.index, a.defaultVal) +} + +func (intArgExpr) hasTarget() bool { return false } + +// IntArg creates an expression referencing the integer value of the argument at the given index. +// +// At runtime, an argument which is not an integer reads as defaultVal, and a negative integer reads +// as 0. When estimating, a literal argument reads as its value, and a non-literal argument is +// unbounded from model version 1 (or FixedSizeEstimate(defaultVal) at version 0). +func IntArg(index int, defaultVal uint64) QuantityExpr { + return intArgExpr{index: index, defaultVal: defaultVal} +} + +// targetExpr represents the size of the receiver/target object. +type targetExpr struct{} + +func (targetExpr) estimate(ctx EstimateContext) SizeEstimate { + if sz, ok := ctx.Target(); ok { + return sz + } + return UnknownSizeEstimate() +} + +func (targetExpr) track(ctx TrackContext) uint64 { + return ctx.Target() +} + +func (targetExpr) eachValue(ctx TrackContext, fn func(ref.Val)) { + if val, ok := ctx.TargetVal(); ok { + fn(val) + } +} + +func (targetExpr) hasTarget() bool { return true } + +// Target creates an expression referencing the size of the receiver / target object. +func Target() QuantityExpr { + return targetExpr{} +} + +// TargetElem creates an expression referencing the element size of the receiver / target object. +func TargetElem() QuantityExpr { + return ElemOf(Target()) +} + +// elemExpr represents the element size of another quantity expression. +type elemExpr struct { + expr QuantityExpr +} + +func (e elemExpr) estimate(ctx EstimateContext) SizeEstimate { + sz := e.expr.estimate(ctx) + if sz.Elem != nil { + return *sz.Elem + } + return UnknownSizeEstimate() +} + +// track reports the size of the largest element, matching the estimate, whose Max bounds every +// element rather than describing an average one. +// +// A value with no elements, including a scalar, contributes nothing. The cost of the scan is +// linear in the number of elements; see the element projection notes in docs/cost-model.md. +func (e elemExpr) track(ctx TrackContext) uint64 { + largest := uint64(0) + e.eachValue(ctx, func(elem ref.Val) { + largest = max(largest, ctx.Size(elem)) + }) + return largest +} + +func (e elemExpr) eachValue(ctx TrackContext, fn func(ref.Val)) { + eachValueOf(e.expr, ctx, func(val ref.Val) { + eachElement(val, fn) + }) +} + +func (e elemExpr) hasTarget() bool { + return hasTarget(e.expr) +} + +// ElemOf creates an expression referencing the element size of another expression. +func ElemOf(expr QuantityExpr) QuantityExpr { + return elemExpr{expr: expr} +} + +// elemTotalExpr represents the summed size of the elements of another quantity expression. +type elemTotalExpr struct { + expr QuantityExpr +} + +func (e elemTotalExpr) estimate(ctx EstimateContext) SizeEstimate { + sz := e.expr.estimate(ctx) + return totalEstimate(ctx, e.expr, sz, sz.Elem, isListOperand(ctx, e.expr, sz)) +} + +// singleValueOf resolves the value an ElemTotal, KeyTotal or Length operand denotes. Validate +// restricts those operands to Arg, Target and Result. +func singleValueOf(expr QuantityExpr, ctx TrackContext) (ref.Val, bool) { + switch x := expr.(type) { + case argExpr: + return ctx.ArgVal(x.index) + case targetExpr: + return ctx.TargetVal() + case resultExpr: + return ctx.ResultVal() + } + return nil, false +} + +// sumSizes totals the sizes of the parts which each enumerates in the operand's value. +func sumSizes(ctx TrackContext, operand QuantityExpr, each func(ref.Val, func(ref.Val))) uint64 { + total := uint64(0) + if val, ok := singleValueOf(operand, ctx); ok { + each(val, func(part ref.Val) { + total = SafeAdd(total, ctx.Size(part)) + }) + } + return total +} + +func (e elemTotalExpr) track(ctx TrackContext) uint64 { + return sumSizes(ctx, e.expr, eachElement) +} + +func (e elemTotalExpr) hasTarget() bool { + return hasTarget(e.expr) +} + +// ElemTotal creates an expression referencing the combined size of every element of a list, or +// every value of a map, under the sizing strategy in effect: the length of a flattened list under +// the default strategy, or the content size of a list under the aggregate strategy. See KeyTotal +// for the keys of a map. +// +// The operand must be Arg, Target or Result; see OverloadModel.Validate. The estimate is derived +// from the operand's size under the sizing strategy; see totalEstimate. At runtime this reports +// the exact total. +func ElemTotal(expr QuantityExpr) QuantityExpr { + return elemTotalExpr{expr: expr} +} + +// keyTotalExpr represents the summed size of the keys of another quantity expression. +type keyTotalExpr struct { + expr QuantityExpr +} + +func (k keyTotalExpr) estimate(ctx EstimateContext) SizeEstimate { + sz := k.expr.estimate(ctx) + return totalEstimate(ctx, k.expr, sz, sz.Key, false) +} + +func (k keyTotalExpr) track(ctx TrackContext) uint64 { + return sumSizes(ctx, k.expr, eachKey) +} + +func (k keyTotalExpr) hasTarget() bool { + return hasTarget(k.expr) +} + +// KeyTotal creates an expression referencing the combined size of every key of a map. A value +// which is not a map has no keys. +// +// The operand must be Arg, Target or Result; see OverloadModel.Validate. The estimate is derived +// from the operand's size under the sizing strategy; see totalEstimate. At runtime this reports +// the exact total. +func KeyTotal(expr QuantityExpr) QuantityExpr { + return keyTotalExpr{expr: expr} +} + +// lengthExpr represents the shallow top-level length of a single value (its CEL size()): the +// number of code points of a string, the number of bytes of a bytes value, the number of top-level +// elements of a list or entries of a map, and 1 for any other value. +type lengthExpr struct { + expr QuantityExpr +} + +// Length creates an expression referencing the shallow top-level length of a single value (its CEL +// size() / ActualSize), independent of the sizing strategy: the number of code points of a string, +// bytes of a bytes value, top-level elements of a list or entries of a map, and 1 for any other +// value. Unlike Arg(i), Target(), or Result(), which report the depth-based total content size +// under AggregateSizingStrategy, Length always reports only the top-level container or value +// length. +// +// The operand must be Arg, Target or Result; see OverloadModel.Validate. Length is a scalar and +// carries no element or key size. +// +// Sizes follow the sizing strategy, so under the aggregate strategy Target() of a string is its +// UTF-8 byte count and Target() of a list is its depth-based aggregate content size. Length lets a +// model charge by top-level length under every strategy while still describing its result's size +// through List and Map. +func Length(expr QuantityExpr) QuantityExpr { + return lengthExpr{expr: expr} +} + +// estimate reports the exact length of a literal operand, as ActualSize counts it at runtime, or +// bounds the operand's length from its estimated size under the sizing strategy. +// +// Because track returns ActualSize(val)—which is 0 for empty strings, bytes, and containers, and 1 +// for fixed-width scalars—the returned range is [1, 1] for fixed-width scalars and [lo, hi] within +// [0, MaxUint64] for variable-length values. For soundness (est.Min <= actual), lo must be 0 +// whenever the operand may be empty at runtime: +// +// - Default strategy: the size is the length ([sz.Min, sz.Max]). +// - Aggregate strategy: every value has an aggregate size >= 1 even when its length is 0 (a +// string's size is max(1, UTF-8 byte count) with 1–4 bytes per code point; a bytes value's size +// is max(1, byte length); a container's size is 1 + entry sizes, see countBounds); any other +// value has length 1. An aggregate size with Min <= 1 therefore yields lo = 0. +// - Other strategies: [0, MaxUint64], except for scalars whose length is always [1, 1]. +// +// The kind of value comes from the operand's checked type, or, for a result whose type is not +// known, from its metadata: an estimate which states element or key sizes is a container. +func (l lengthExpr) estimate(ctx EstimateContext) SizeEstimate { + if lit, ok := operandLiteral(ctx, l.expr); ok { + return FixedSizeEstimate(ActualSize(lit)) + } + sz := l.expr.estimate(ctx) + kind, known := operandKind(ctx, l.expr) + if known && hasUnitLength(kind) { + return FixedSizeEstimate(1) + } + switch strategyOf(ctx).(type) { + case defaultSizingStrategy: + return RangedSizeEstimate(sz.Min, sz.Max) + case *aggregateSizingStrategy: + switch { + case known && kind == types.StringKind: + // Size <= 1 may be "" (length 0); n > 1 UTF-8 bytes hold [ceil(n/4), n] code points. + lo := uint64(0) + if sz.Min > 1 { + lo = sz.Min/4 + min(sz.Min%4, 1) + } + return RangedSizeEstimate(lo, sz.Max) + case known && kind == types.BytesKind: + // Size <= 1 may be b"" (length 0). + lo := uint64(0) + if sz.Min > 1 { + lo = sz.Min + } + return RangedSizeEstimate(lo, sz.Max) + case (known && (kind == types.ListKind || kind == types.MapKind)) || sz.Elem != nil || sz.Key != nil: + return countBounds(strategyOf(ctx), sz) + default: + // Any value: empty strings/bytes/containers have length 0, and length never exceeds size (>= 1). + return RangedSizeEstimate(0, max(sz.Max, 1)) + } + default: + return UnknownSizeEstimate() + } +} + +func (l lengthExpr) track(ctx TrackContext) uint64 { + val, ok := singleValueOf(l.expr, ctx) + // A call which fails produces an error rather than a result, which has no length. + if !ok || types.IsUnknownOrError(val) { + return 0 + } + // An optional has the length of the value it holds, as it is estimated. + if inner, isOpt := optionalValueOf(val); isOpt { + if inner == nil { + return 0 + } + val = inner + } + return ActualSize(val) +} + +func (l lengthExpr) hasTarget() bool { + return hasTarget(l.expr) +} + +// TargetKey creates an expression referencing the key size of the receiver / target object. +func TargetKey() QuantityExpr { + return KeyOf(Target()) +} + +// intTargetExpr represents the integer value of the receiver/target object. +type intTargetExpr struct { + defaultVal uint64 +} + +func (t intTargetExpr) estimate(ctx EstimateContext) SizeEstimate { + if versionOf(ctx) < 1 { + return FixedSizeEstimate(ctx.TargetValue(t.defaultVal)) + } + lo, hi := ctx.TargetValue(0), ctx.TargetValue(math.MaxUint64) + if lo != hi { + return UnknownSizeEstimate() + } + return FixedSizeEstimate(lo) +} + +func (t intTargetExpr) track(ctx TrackContext) uint64 { + return ctx.TargetValue(t.defaultVal) +} + +func (intTargetExpr) hasTarget() bool { return true } + +// IntTarget creates an expression referencing the integer value of the receiver/target object, +// with the same runtime and estimation rules as IntArg. +func IntTarget(defaultVal uint64) QuantityExpr { + return intTargetExpr{defaultVal: defaultVal} +} + +// keyExpr represents the key size of another quantity expression. +type keyExpr struct { + expr QuantityExpr +} + +func (k keyExpr) estimate(ctx EstimateContext) SizeEstimate { + sz := k.expr.estimate(ctx) + if sz.Key != nil { + return *sz.Key + } + // An absent key size is unknown rather than single-byte: map keys may be strings or bytes of + // any length. Strategies which know the key type is fixed-width populate Key themselves. + return UnknownSizeEstimate() +} + +// track reports the size of the largest key, matching the estimate, whose Max bounds every key +// rather than describing an average one. +// +// A value with no keys, including a list or a scalar, contributes nothing. The cost of the scan is +// linear in the number of entries; see the element projection notes in docs/cost-model.md. +func (k keyExpr) track(ctx TrackContext) uint64 { + largest := uint64(0) + k.eachValue(ctx, func(key ref.Val) { + largest = max(largest, ctx.Size(key)) + }) + return largest +} + +func (k keyExpr) eachValue(ctx TrackContext, fn func(ref.Val)) { + eachValueOf(k.expr, ctx, func(val ref.Val) { + eachKey(val, fn) + }) +} + +func (k keyExpr) hasTarget() bool { + return hasTarget(k.expr) +} + +// KeyOf creates an expression referencing the key size of another expression. +func KeyOf(expr QuantityExpr) QuantityExpr { + return keyExpr{expr: expr} +} + +// resultExpr represents the size of the evaluated result. +type resultExpr struct{} + +func (resultExpr) estimate(ctx EstimateContext) SizeEstimate { + if sz, ok := ctx.Result(); ok { + return sz + } + return UnknownSizeEstimate() +} + +func (resultExpr) track(ctx TrackContext) uint64 { + return ctx.Result() +} + +func (resultExpr) eachValue(ctx TrackContext, fn func(ref.Val)) { + if val, ok := ctx.ResultVal(); ok { + fn(val) + } +} + +func (resultExpr) hasTarget() bool { return false } + +// Result creates an expression referencing the size of the result. +func Result() QuantityExpr { + return resultExpr{} +} + +// addExpr represents the sum of multiple quantity expressions. +type addExpr struct { + terms []QuantityExpr +} + +func (a addExpr) estimate(ctx EstimateContext) SizeEstimate { + if len(a.terms) == 0 { + return FixedSizeEstimate(0) + } + return foldEstimates(ctx, a.terms, SizeEstimate.Add) +} + +func (a addExpr) track(ctx TrackContext) uint64 { + sum := uint64(0) + for _, term := range a.terms { + sum = SafeAdd(sum, term.track(ctx)) + } + return sum +} + +func (a addExpr) hasTarget() bool { + return slices.ContainsFunc(a.terms, hasTarget) +} + +// Sum creates an expression representing the sum of the given terms. +func Sum(terms ...QuantityExpr) QuantityExpr { + return addExpr{terms: slices.Clone(terms)} +} + +// subExpr represents the subtraction of two quantity expressions. +type subExpr struct { + lhs, rhs QuantityExpr +} + +func (s subExpr) estimate(ctx EstimateContext) SizeEstimate { + lhsVal := s.lhs.estimate(ctx) + rhsVal := s.rhs.estimate(ctx) + diff := lhsVal.Subtract(rhsVal) + // A difference holds some of the minuend's elements, never the subtrahend's. + diff.Key, diff.Elem = lhsVal.Key, lhsVal.Elem + return diff +} + +func (s subExpr) track(ctx TrackContext) uint64 { + return SafeSubtract(s.lhs.track(ctx), s.rhs.track(ctx)) +} + +func (s subExpr) hasTarget() bool { + return hasTarget(s.lhs) || hasTarget(s.rhs) +} + +// Sub creates an expression representing lhs - rhs, saturated at zero. +func Sub(lhs, rhs QuantityExpr) QuantityExpr { + return subExpr{lhs: lhs, rhs: rhs} +} + +// mulExpr represents the product of multiple quantity expressions. +type mulExpr struct { + terms []QuantityExpr +} + +func (m mulExpr) estimate(ctx EstimateContext) SizeEstimate { + if len(m.terms) == 0 { + return FixedSizeEstimate(1) + } + return foldEstimates(ctx, m.terms, SizeEstimate.Multiply) +} + +func (m mulExpr) track(ctx TrackContext) uint64 { + if len(m.terms) == 0 { + return 1 + } + prod := m.terms[0].track(ctx) + for _, term := range m.terms[1:] { + prod = SafeMultiply(prod, term.track(ctx)) + } + return prod +} + +func (m mulExpr) hasTarget() bool { + return slices.ContainsFunc(m.terms, hasTarget) +} + +// Mul creates an expression representing the product of the given terms. +func Mul(terms ...QuantityExpr) QuantityExpr { + return mulExpr{terms: slices.Clone(terms)} +} + +// scaleExpr represents an expression scaled by a constant factor. +type scaleExpr struct { + expr QuantityExpr + factor float64 + // rat is the exact rational form of factor; see rationalOf. + rat rational +} + +func (s scaleExpr) estimate(ctx EstimateContext) SizeEstimate { + val := s.expr.estimate(ctx) + res := RangedSizeEstimate( + scale(val.Min, s.factor, s.rat), + scale(val.Max, s.factor, s.rat), + ) + res.Key = val.Key + res.Elem = val.Elem + return res +} + +func (s scaleExpr) track(ctx TrackContext) uint64 { + return scale(s.expr.track(ctx), s.factor, s.rat) +} + +func (s scaleExpr) hasTarget() bool { + return hasTarget(s.expr) +} + +// Scale creates an expression that scales another expression by a constant factor, rounding up. +// +// Factors are constant so that a model's runtime charge depends only on the sizes reported by the +// sizing strategy. A cost which differs by argument type should be modeled with one OverloadModel +// per overload ID, since CEL already dispatches overloads by type. The factor must be finite and +// positive; Validate rejects any other. +// +// The factor is applied as the simplest rational with the same float64 value, so that 0.1 scales by +// exactly 1/10, and the product is computed exactly; an unbounded quantity stays unbounded; and +// when a model is compiled, factors are moved outward so that each product is rounded once. +func Scale(expr QuantityExpr, factor float64) QuantityExpr { + return scaleExpr{expr: expr, factor: factor, rat: rationalOf(factor)} +} + +// Square creates an expression representing the square of another expression. +func Square(expr QuantityExpr) QuantityExpr { + return Mul(expr, expr) +} + +// minExpr represents the minimum of two quantity expressions. +type minExpr struct { + lhs, rhs QuantityExpr +} + +func (m minExpr) estimate(ctx EstimateContext) SizeEstimate { + return foldEstimates(ctx, []QuantityExpr{m.lhs, m.rhs}, minBounds) +} + +func (m minExpr) track(ctx TrackContext) uint64 { + return min(m.lhs.track(ctx), m.rhs.track(ctx)) +} + +func (m minExpr) hasTarget() bool { + return hasTarget(m.lhs) || hasTarget(m.rhs) +} + +// Min creates an expression representing the minimum of two quantities. +func Min(lhs, rhs QuantityExpr) QuantityExpr { + return minExpr{lhs: lhs, rhs: rhs} +} + +// maxExpr represents the maximum of two quantity expressions. +type maxExpr struct { + lhs, rhs QuantityExpr +} + +func (m maxExpr) estimate(ctx EstimateContext) SizeEstimate { + return foldEstimates(ctx, []QuantityExpr{m.lhs, m.rhs}, maxBounds) +} + +func (m maxExpr) track(ctx TrackContext) uint64 { + return max(m.lhs.track(ctx), m.rhs.track(ctx)) +} + +func (m maxExpr) hasTarget() bool { + return hasTarget(m.lhs) || hasTarget(m.rhs) +} + +// Max creates an expression representing the maximum of two quantities. +func Max(lhs, rhs QuantityExpr) QuantityExpr { + return maxExpr{lhs: lhs, rhs: rhs} +} + +// unionExpr represents the union interval of multiple quantity expressions. +type unionExpr struct { + terms []QuantityExpr +} + +func (u unionExpr) estimate(ctx EstimateContext) SizeEstimate { + if len(u.terms) == 0 { + return FixedSizeEstimate(0) + } + return foldEstimates(ctx, u.terms, SizeEstimate.Union) +} + +func (u unionExpr) track(ctx TrackContext) uint64 { + res := uint64(0) + for _, term := range u.terms { + res = max(res, term.track(ctx)) + } + return res +} + +func (u unionExpr) hasTarget() bool { + return slices.ContainsFunc(u.terms, hasTarget) +} + +// Union creates an expression representing the union interval of multiple expressions. +func Union(terms ...QuantityExpr) QuantityExpr { + return unionExpr{terms: slices.Clone(terms)} +} + +// rangedExpr represents a quantity bounded by minExpr on the bottom and maxExpr on top. +type rangedExpr struct { + minExpr QuantityExpr + maxExpr QuantityExpr +} + +func (r rangedExpr) estimate(ctx EstimateContext) SizeEstimate { + return foldEstimates(ctx, []QuantityExpr{r.minExpr, r.maxExpr}, rangedBounds) +} + +// foldEstimates estimates the operands and combines the estimates from left to right with op, +// attaching the operands' metadata as combineMeta describes. The estimates of up to four operands, +// as many as the models in this repository use, are held on the stack. +func foldEstimates(ctx EstimateContext, operands []QuantityExpr, op func(SizeEstimate, SizeEstimate) SizeEstimate) SizeEstimate { + var buf [4]SizeEstimate + ests := buf[:0] + for _, operand := range operands { + ests = append(ests, operand.estimate(ctx)) + } + res := ests[0] + for _, est := range ests[1:] { + res = op(res, est) + } + return combineMeta(ctx, RangedSizeEstimate(res.Min, res.Max), operands, ests) +} + +func minBounds(lhs, rhs SizeEstimate) SizeEstimate { + return RangedSizeEstimate(min(lhs.Min, rhs.Min), min(lhs.Max, rhs.Max)) +} + +func maxBounds(lhs, rhs SizeEstimate) SizeEstimate { + return RangedSizeEstimate(max(lhs.Min, rhs.Min), max(lhs.Max, rhs.Max)) +} + +// rangedBounds takes the upper bound from hi and the lower bound from lo, capped by the upper bound +// since a lower bound above it would describe an empty interval. +func rangedBounds(lo, hi SizeEstimate) SizeEstimate { + return RangedSizeEstimate(min(lo.Min, hi.Max), hi.Max) +} + +func (r rangedExpr) track(ctx TrackContext) uint64 { + return r.maxExpr.track(ctx) +} + +func (r rangedExpr) hasTarget() bool { + return hasTarget(r.minExpr) || hasTarget(r.maxExpr) +} + +// Ranged creates an expression where the lower bound is taken from minExpr and the upper bound from maxExpr. +func Ranged(minExpr, maxExpr QuantityExpr) QuantityExpr { + return rangedExpr{minExpr: minExpr, maxExpr: maxExpr} +} + +// AtMost creates an expression representing a quantity bounded between 0 and the upper bound of maxExpr. +func AtMost(maxExpr QuantityExpr) QuantityExpr { + return Ranged(Const(0), maxExpr) +} + +// atLeastOneExpr represents a quantity guaranteed to be at least 1. +type atLeastOneExpr struct { + expr QuantityExpr +} + +func (a atLeastOneExpr) estimate(ctx EstimateContext) SizeEstimate { + return AtLeastOneSize(a.expr.estimate(ctx)) +} + +func (a atLeastOneExpr) track(ctx TrackContext) uint64 { + val := a.expr.track(ctx) + if val == 0 { + return 1 + } + return val +} + +func (a atLeastOneExpr) hasTarget() bool { + return hasTarget(a.expr) +} + +// AtLeastOneQuantity creates an expression ensuring the quantity is at least 1. +func AtLeastOneQuantity(expr QuantityExpr) QuantityExpr { + return atLeastOneExpr{expr: expr} +} + +// StringScan creates an expression representing the cost of scanning a string. +func StringScan(expr QuantityExpr) QuantityExpr { + return Scale(expr, StringTraversalCostFactor) +} + +// ListAlloc creates an expression representing list allocation cost with base cost and scaled element count. +func ListAlloc(elemCount QuantityExpr, costFactor float64) QuantityExpr { + return Sum(Const(ListCreateBaseCost), Scale(elemCount, costFactor)) +} + +// AllocList creates an expression representing the cost of a call that allocates a list: +// CallCost + ListCreateBaseCost plus the given work terms. +func AllocList(terms ...QuantityExpr) QuantityExpr { + all := make([]QuantityExpr, 0, len(terms)+1) + all = append(all, Const(CallCost+ListCreateBaseCost)) + all = append(all, terms...) + return Sum(all...) +} + +// Traversal creates an expression representing the traversal and optional allocation cost over an expression. +func Traversal(expr QuantityExpr, costFactor float64, allocCost uint64) QuantityExpr { + if allocCost == 0 { + return Scale(expr, costFactor) + } + return Sum(Const(allocCost), Scale(expr, costFactor)) +} + +// listExpr represents a list size estimate composed of length and element size expressions. +type listExpr struct { + lenExpr QuantityExpr + elemExpr QuantityExpr +} + +func (l listExpr) estimate(ctx EstimateContext) SizeEstimate { + lenSize := l.lenExpr.estimate(ctx) + elemSize := l.elemExpr.estimate(ctx) + size := containerSize(ctx, lenSize, elemSize) + size.Key = nil + size.Elem = &elemSize + return size +} + +func (l listExpr) track(ctx TrackContext) uint64 { + return l.lenExpr.track(ctx) +} + +func (l listExpr) hasTarget() bool { + return hasTarget(l.lenExpr) || hasTarget(l.elemExpr) +} + +// List creates an expression representing a list with the given length and element size. +// +// The estimate is the size the sizing strategy assigns to such a list: its length under the default +// strategy, and 1 plus the length times the element size under the aggregate strategy. The track is +// the length. +func List(lenExpr, elemExpr QuantityExpr) QuantityExpr { + return listExpr{lenExpr: lenExpr, elemExpr: elemExpr} +} + +// mapExpr represents a map size estimate composed of size, key size, and value size expressions. +type mapExpr struct { + sizeExpr QuantityExpr + keyExpr QuantityExpr + valExpr QuantityExpr +} + +func (m mapExpr) estimate(ctx EstimateContext) SizeEstimate { + mapSize := m.sizeExpr.estimate(ctx) + keySize := m.keyExpr.estimate(ctx) + valSize := m.valExpr.estimate(ctx) + size := containerSize(ctx, mapSize, AtLeastOneSize(keySize).Add(AtLeastOneSize(valSize))) + size.Key = &keySize + size.Elem = &valSize + return size +} + +func (m mapExpr) track(ctx TrackContext) uint64 { + return m.sizeExpr.track(ctx) +} + +func (m mapExpr) hasTarget() bool { + return hasTarget(m.sizeExpr) || hasTarget(m.keyExpr) || hasTarget(m.valExpr) +} + +// Map creates an expression representing a map with the given entry count, key size and value size. +// +// The estimate is the size the sizing strategy assigns to such a map: its entry count under the +// default strategy, and 1 plus the entry count times the key and value sizes under the aggregate +// strategy. The track is the entry count. +func Map(sizeExpr, keyExpr, valExpr QuantityExpr) QuantityExpr { + return mapExpr{sizeExpr: sizeExpr, keyExpr: keyExpr, valExpr: valExpr} +} + +// optionalExpr represents the size of an optional which holds a value of the given size. +type optionalExpr struct { + valueExpr QuantityExpr +} + +func (o optionalExpr) estimate(ctx EstimateContext) SizeEstimate { + if versionOf(ctx) < 1 { + return FixedSizeEstimate(1) + } + return optionalSizeOf(o.valueExpr.estimate(ctx)) +} + +func (o optionalExpr) track(ctx TrackContext) uint64 { + if trackVersionOf(ctx) < 1 { + return 1 + } + return SafeAdd(1, o.valueExpr.track(ctx)) +} + +func (o optionalExpr) hasTarget() bool { + return hasTarget(o.valueExpr) +} + +// Optional creates an expression representing an optional which holds a value of the given size. +// +// From cost model version 1 an optional is 1 plus the size of the value it holds, under every +// sizing strategy: 1 plus the value's length under the default strategy, and 1 plus its aggregate +// size under the aggregate strategy. An empty optional is 1, which Optional(Const(0)) describes. +// The value is the optional's element, which ElemOf reads, so that value() and orValue() recover its +// size. Version 0 sizes every optional as 1, as cel-go v0.32.0 did. +func Optional(valueExpr QuantityExpr) QuantityExpr { + return optionalExpr{valueExpr: valueExpr} +} + +// OverloadModel pairs an overload ID with its abstract cost and result size expressions. +// +// An OverloadModel is a value built by Overload or MemberOverload, and is not modified afterwards: +// estimators and trackers are compiled from it on request. Validate reports a model which cannot +// be priced, and cel.CostModel and Model call it when the model is registered. +type OverloadModel struct { + ID string + Cost QuantityExpr + Size QuantityExpr + hasSize bool + IsMember bool + manual bool + manualEstimator bool + // argumentsOnly marks a short-circuiting operator, whose cost is that of the arguments it + // evaluates, and which is therefore exempt from the minimum call cost; see minCallCost. + argumentsOnly bool + estimator FunctionEstimator + tracker FunctionTracker + priors []priorOverload +} + +// priorOverload is the model which priced an overload at revisions earlier than before. +type priorOverload struct { + before uint32 + model OverloadModel +} + +// priorForVersion selects the prior with the smallest before which still exceeds version, so +// that each prior covers the revisions from the previous prior's before up to its own. +func priorForVersion(priors []priorOverload, version uint32) (OverloadModel, bool) { + var chosen *priorOverload + for i, p := range priors { + if version < p.before && (chosen == nil || p.before < chosen.before) { + chosen = &priors[i] + } + } + if chosen == nil { + return OverloadModel{}, false + } + return chosen.model, true +} + +// OverloadModelOption functional options for configuring an OverloadModel. +type OverloadModelOption func(*OverloadModel) + +// EvalCost sets the evaluation cost expression for the overload model. +func EvalCost(cost QuantityExpr) OverloadModelOption { + return func(m *OverloadModel) { + m.Cost = cost + } +} + +// ResultSize sets the result size expression for the overload model. Passing nil in a +// BeforeVersion entry clears any result size inherited from the enclosing OverloadModel. +func ResultSize(size QuantityExpr) OverloadModelOption { + return func(m *OverloadModel) { + m.Size = size + m.hasSize = true + } +} + +// ManualCost sets a custom FunctionEstimator and FunctionTracker for an overload whose cost or +// result size cannot be expressed declaratively with EvalCost and ResultSize. +// +// Both functions are required; Validate reports a nil one. ManualCost cannot be combined with +// EvalCost, ResultSize, or ManualEstimator on the same OverloadModel or BeforeVersion entry. +func ManualCost(estimator FunctionEstimator, tracker FunctionTracker) OverloadModelOption { + return func(m *OverloadModel) { + m.manual = true + m.estimator = estimator + m.tracker = tracker + } +} + +// ManualEstimator sets a custom FunctionEstimator for an overload (or BeforeVersion entry) whose +// static estimate cannot be expressed declaratively with EvalCost and ResultSize, while retaining +// the OverloadModel's declarative EvalCost for runtime tracking. +// +// ManualEstimator requires a non-nil FunctionEstimator and cannot be combined with ManualCost or +// ResultSize on the same OverloadModel or BeforeVersion entry. +func ManualEstimator(estimator FunctionEstimator) OverloadModelOption { + return func(m *OverloadModel) { + m.manualEstimator = true + m.estimator = estimator + } +} + +// BeforeVersion records the options that priced the overload before the given model revision. +// +// At revisions earlier than version, the overload is configured by opts (such as EvalCost, +// ResultSize, ManualCost, or ManualEstimator), inheriting the enclosing OverloadModel's ID, +// receiver kind, and any unset EvalCost or ResultSize. +func BeforeVersion(version uint32, opts ...OverloadModelOption) OverloadModelOption { + return func(m *OverloadModel) { + m.priors = append(m.priors, priorOverload{ + before: version, + model: newOverloadModel(m.ID, m.IsMember, opts), + }) + } +} + +// resolvePrior returns prior with any unset Cost or Size inherited from m. +func (m OverloadModel) resolvePrior(prior OverloadModel) OverloadModel { + if prior.manual { + return prior + } + if prior.Cost == nil { + prior.Cost = m.Cost + } + if !prior.manualEstimator && !prior.hasSize { + prior.Size = m.Size + prior.hasSize = m.hasSize + } + return prior +} + +// Overload creates a global function OverloadModel with functional options. +func Overload(id string, opts ...OverloadModelOption) OverloadModel { + return newOverloadModel(id, false, opts) +} + +// MemberOverload creates a member function OverloadModel with functional options. +func MemberOverload(id string, opts ...OverloadModelOption) OverloadModel { + return newOverloadModel(id, true, opts) +} + +func newOverloadModel(id string, isMember bool, opts []OverloadModelOption) OverloadModel { + m := OverloadModel{ID: id, IsMember: isMember} + for _, opt := range opts { + opt(&m) + } + return m +} + +func (m OverloadModel) hasTarget() bool { + return m.IsMember || hasTarget(m.Cost) || hasTarget(m.Size) +} + +var estimatorCtxPool = sync.Pool{ + New: func() any { + return &estimatorEvalContext{} + }, +} + +var trackerCtxPool = sync.Pool{ + New: func() any { + return &trackerEvalContext{} + }, +} + +// FunctionEstimator returns a FunctionEstimator implementing the cost model. +func (m OverloadModel) FunctionEstimator() FunctionEstimator { + return m.buildEstimator(defaultSizing, defaultModelVersion) +} + +// FunctionEstimatorWithOptions returns a FunctionEstimator implementing the cost model, configured +// by the supplied options. Unset options take their defaults: the default SizingStrategy and the +// latest model revision. +func (m OverloadModel) FunctionEstimatorWithOptions(opts ...ModelOption) FunctionEstimator { + resolved := newModelOptions(opts...) + return m.buildEstimator(resolved.strategy, resolved.version) +} + +func (m OverloadModel) buildEstimator(strategy SizingStrategy, version uint32) FunctionEstimator { + if prior, ok := priorForVersion(m.priors, version); ok { + return m.resolvePrior(prior).buildEstimator(strategy, version) + } + if m.manual || m.manualEstimator { + return m.estimator + } + hasTarget := m.hasTarget() + cost := normalize(m.Cost) + size := normalize(m.Size) + floor := m.hasCallFloor(version) + return func(estimator Estimator, target *AstNode, args []AstNode) *CallEstimate { + if hasTarget && target == nil { + return nil + } + ctx := estimatorCtxPool.Get().(*estimatorEvalContext) + if ve, ok := estimator.(*versionedEstimator); ok { + ctx.coster = ve.coster + } + ctx.estimator = estimator + ctx.strategy = strategy + ctx.version = version + ctx.target = target + ctx.args = args + ctx.hasTarget = hasTarget + var resSize *SizeEstimate + if size != nil { + sz := aggregateResultSize(strategy, size.estimate(ctx)) + resSize = &sz + // The result size is available to the cost through Result(). + ctx.result = resSize + } + costEst := cost.estimate(ctx) + *ctx = estimatorEvalContext{} + estimatorCtxPool.Put(ctx) + if floor { + costEst.Min, costEst.Max = max(costEst.Min, minCallCost), max(costEst.Max, minCallCost) + } + return NewCallEstimate(CostEstimate{Min: costEst.Min, Max: costEst.Max}, resSize) + } +} + +// FunctionTracker returns a FunctionTracker implementing the cost model. +func (m OverloadModel) FunctionTracker() FunctionTracker { + return m.buildTracker(defaultSizing, defaultModelVersion) +} + +// FunctionTrackerWithOptions returns a FunctionTracker implementing the cost model, configured by +// the supplied options. +// +// ModelVersion applies to tracking as it does to estimation, so that a call's estimate and its +// charge are computed by the same rules. +func (m OverloadModel) FunctionTrackerWithOptions(opts ...ModelOption) FunctionTracker { + resolved := newModelOptions(opts...) + return m.buildTracker(resolved.strategy, resolved.version) +} + +func (m OverloadModel) buildTracker(strategy SizingStrategy, version uint32) FunctionTracker { + if prior, ok := priorForVersion(m.priors, version); ok { + return m.resolvePrior(prior).buildTracker(strategy, version) + } + if m.manual { + return m.tracker + } + isMember := m.hasTarget() + cost := normalize(m.Cost) + floor := m.hasCallFloor(version) + return func(args []ref.Val, result ref.Val) *uint64 { + ctx := trackerCtxPool.Get().(*trackerEvalContext) + ctx.strategy = strategy + ctx.version = version + ctx.args = args + ctx.result = result + ctx.isMember = isMember + costVal := cost.track(ctx) + *ctx = trackerEvalContext{} + trackerCtxPool.Put(ctx) + if floor { + costVal = max(costVal, minCallCost) + } + return &costVal + } +} + +// versionOf reports the cost model version of an EstimateContext. +func versionOf(ctx EstimateContext) uint32 { + if ctx == nil { + return defaultModelVersion + } + if e, ok := ctx.(*estimatorEvalContext); ok && (e.estimator == nil || e.coster != nil) { + return e.version + } + return ModelVersionOf(ctx.Estimator()) +} + +// estimatorEvalContext provides evaluation context for cost estimation. +type estimatorEvalContext struct { + coster *coster + estimator Estimator + strategy SizingStrategy + version uint32 + target *AstNode + args []AstNode + hasTarget bool + // result is the estimated result size, available to the cost through Result(). + result *SizeEstimate +} + +// sizingStrategy implements sizingStrategyContext. +func (e *estimatorEvalContext) sizingStrategy() SizingStrategy { + return e.strategy +} + +// resultType implements resultTypeContext from the estimator, which knows the call being +// estimated. +func (e *estimatorEvalContext) resultType() (*types.Type, bool) { + if rc, ok := e.estimator.(resultTypeContext); ok { + return rc.resultType() + } + return nil, false +} + +// operandNode implements operandNodeContext for Arg and Target operands. +func (e *estimatorEvalContext) operandNode(q QuantityExpr) (AstNode, bool) { + switch x := q.(type) { + case argExpr: + if x.index >= 0 && x.index < len(e.args) { + return e.args[x.index], true + } + case targetExpr: + if e.target != nil { + return *e.target, true + } + } + return nil, false +} + +// Arg returns the size estimate for the argument at index. +func (e *estimatorEvalContext) Arg(index int) (SizeEstimate, bool) { + if index < len(e.args) { + return e.Size(e.args[index]), true + } + return UnknownSizeEstimate(), false +} + +// Target returns the size estimate for the receiver/target object. +func (e *estimatorEvalContext) Target() (SizeEstimate, bool) { + if e.target != nil { + return e.Size(*e.target), true + } + return UnknownSizeEstimate(), false +} + +// Result returns the size estimate of the result, if the model's result size has been estimated. +func (e *estimatorEvalContext) Result() (SizeEstimate, bool) { + if e.result != nil { + return *e.result, true + } + return UnknownSizeEstimate(), false +} + +// Estimator returns the underlying Estimator. +func (e *estimatorEvalContext) Estimator() Estimator { + return e.estimator +} + +// Size returns the estimated size of an AST node. +func (e *estimatorEvalContext) Size(node AstNode) SizeEstimate { + if node == nil { + return UnknownSizeEstimate() + } + if sz := node.ComputedSize(); sz != nil { + return *sz + } + if e.coster != nil && node.Expr() != nil { + if sz := e.coster.computeSize(node.Expr()); sz != nil { + return *sz + } + } + if sz, ok := e.strategy.EstimateSize(e, node); ok { + return sz + } + if e.estimator != nil { + if sz := e.estimator.EstimateSize(node); sz != nil { + return *sz + } + } + return UnknownSizeEstimate() +} + +// TargetType returns the type of the receiver/target object. +func (e *estimatorEvalContext) TargetType() (*types.Type, bool) { + if e.target != nil && (*e.target) != nil { + return (*e.target).Type(), true + } + return nil, false +} + +// ArgType returns the type of the argument at index. +func (e *estimatorEvalContext) ArgType(index int) (*types.Type, bool) { + if index < len(e.args) && e.args[index] != nil { + return e.args[index].Type(), true + } + return nil, false +} + +// ArgValue returns the uint64 value of the argument at index, or defaultVal if not a literal int/uint. +func (e *estimatorEvalContext) ArgValue(index int, defaultVal uint64) uint64 { + if index < len(e.args) && e.args[index] != nil { + return NodeAsUintValue(e.args[index], defaultVal) + } + return defaultVal +} + +// TargetValue returns the uint64 value of the receiver/target, or defaultVal if not a literal int/uint. +func (e *estimatorEvalContext) TargetValue(defaultVal uint64) uint64 { + if e.target != nil && (*e.target) != nil { + return NodeAsUintValue(*e.target, defaultVal) + } + return defaultVal +} + +// trackerEvalContext provides evaluation context for runtime cost tracking. +type trackerEvalContext struct { + estimator ActualCostEstimator + strategy SizingStrategy + version uint32 + args []ref.Val + result ref.Val + isMember bool +} + +// valueAsUint returns the non-negative uint64 value of ref.Val (int or uint), or defaultVal. +func valueAsUint(val ref.Val, defaultVal uint64) uint64 { + if val == nil { + return defaultVal + } + switch v := val.(type) { + case types.Int: + if v < 0 { + return 0 + } + return uint64(v) + case types.Uint: + return uint64(v) + default: + return defaultVal + } +} + +// ArgValue returns the uint64 value of the argument at index, or defaultVal. +func (t *trackerEvalContext) ArgValue(index int, defaultVal uint64) uint64 { + idx := index + if t.isMember { + idx = index + 1 + } + if idx < len(t.args) { + return valueAsUint(t.args[idx], defaultVal) + } + return defaultVal +} + +// TargetValue returns the uint64 value of the receiver/target object, or defaultVal. +func (t *trackerEvalContext) TargetValue(defaultVal uint64) uint64 { + if t.isMember && len(t.args) > 0 { + return valueAsUint(t.args[0], defaultVal) + } + return defaultVal +} + +// TargetType returns the type of the receiver/target object. +func (t *trackerEvalContext) TargetType() (*types.Type, bool) { + if t.isMember && len(t.args) > 0 && t.args[0] != nil { + if tp, ok := t.args[0].Type().(*types.Type); ok { + return tp, true + } + } + return nil, false +} + +// ArgType returns the type of the argument at index. +func (t *trackerEvalContext) ArgType(index int) (*types.Type, bool) { + idx := index + if t.isMember { + idx = index + 1 + } + if idx < len(t.args) && t.args[idx] != nil { + if tp, ok := t.args[idx].Type().(*types.Type); ok { + return tp, true + } + } + return nil, false +} + +// Estimator returns the runtime ActualCostEstimator. +func (t *trackerEvalContext) Estimator() ActualCostEstimator { + return t.estimator +} + +// Size returns the actual runtime size of a value. +func (t *trackerEvalContext) Size(value ref.Val) uint64 { + if value == nil { + return 0 + } + // Inlines defaultSizingStrategy.TrackSize, avoiding an interface call and boxing the context. + if t.strategy == defaultSizing { + if opt, isOpt := value.(*types.Optional); isOpt { + return optionalLength(t.version, opt) + } + return ActualSize(value) + } + if sz, ok := t.strategy.TrackSize(t, value); ok { + return sz + } + return ActualSize(value) +} + +// ArgVal returns the value of the argument at index. +func (t *trackerEvalContext) ArgVal(index int) (ref.Val, bool) { + idx := index + if t.isMember { + idx = index + 1 + } + if idx < len(t.args) && t.args[idx] != nil { + return t.args[idx], true + } + return nil, false +} + +// TargetVal returns the value of the receiver/target object. +func (t *trackerEvalContext) TargetVal() (ref.Val, bool) { + if t.isMember && len(t.args) > 0 && t.args[0] != nil { + return t.args[0], true + } + return nil, false +} + +// ResultVal returns the evaluated result value. +func (t *trackerEvalContext) ResultVal() (ref.Val, bool) { + if t.result != nil { + return t.result, true + } + return nil, false +} + +// Arg returns the runtime size of the argument at index. +func (t *trackerEvalContext) Arg(index int) uint64 { + if val, ok := t.ArgVal(index); ok { + return t.Size(val) + } + return 0 +} + +// Target returns the runtime size of the receiver/target object. +func (t *trackerEvalContext) Target() uint64 { + if val, ok := t.TargetVal(); ok { + return t.Size(val) + } + return 0 +} + +// Result returns the runtime size of the result. +func (t *trackerEvalContext) Result() uint64 { + if val, ok := t.ResultVal(); ok { + return t.Size(val) + } + return 0 +} + +// modelVersion implements trackVersionContext. +func (t *trackerEvalContext) modelVersion() uint32 { + return t.version +} + +// minCallCost is the least a call priced by an OverloadModel costs from cost model version 1, both +// estimated and tracked: a call does at least a unit of work, even over empty arguments, as +// comparing two empty strings does. The short-circuiting operators, ||, && and ?:, and the optional +// or() and orValue(), are exempt, as their cost is that of the arguments they evaluate. +// +// Version 0 reproduces cel-go v0.32.0, which estimated and charged such calls 0. +const minCallCost = uint64(1) + +// hasCallFloor reports whether the model's declarative cost is raised to minCallCost at version. +func (m OverloadModel) hasCallFloor(version uint32) bool { + return version >= 1 && !m.argumentsOnly +} + +// argumentsOnly marks the model of a short-circuiting operator, which is exempt from minCallCost. +func argumentsOnly() OverloadModelOption { + return func(m *OverloadModel) { + m.argumentsOnly = true + } +} diff --git a/vendor/cel.dev/cel-go/common/cost/standard.go b/vendor/cel.dev/cel-go/common/cost/standard.go new file mode 100644 index 00000000000..bceb25c15d3 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/standard.go @@ -0,0 +1,278 @@ +// 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 cost + +import "cel.dev/cel-go/common/overloads" + +// StandardOverloadModels defines the cost models for standard CEL functions. +var StandardOverloadModels = []OverloadModel{ + // O(1) container index operations + Overload(overloads.IndexList, + EvalCost(Const(1)), + ResultSize(ArgElem(0)), + ), + Overload(overloads.IndexMap, + EvalCost(Const(1)), + ResultSize(ArgElem(0)), + ), + + // O(n) prefix/suffix functions + MemberOverload(overloads.StartsWithString, + EvalCost(Scale(Arg(0), StringTraversalCostFactor)), + ), + MemberOverload(overloads.EndsWithString, + EvalCost(Scale(Arg(0), StringTraversalCostFactor)), + ), + + // O(n) conversion & format functions + Overload(overloads.StringToBytes, + EvalCost(Scale(Arg(0), StringTraversalCostFactor)), + ResultSize(Ranged(Arg(0), Scale(Arg(0), 4.0))), + ), + Overload(overloads.BytesToString, + EvalCost(Scale(Arg(0), StringTraversalCostFactor)), + ResultSize(Ranged(Scale(Arg(0), 0.25), Arg(0))), + ), + Overload(overloads.ExtQuoteString, + EvalCost(Scale(Arg(0), StringTraversalCostFactor)), + ResultSize(Ranged(Sum(Arg(0), Const(2)), Sum(Scale(Arg(0), 2.0), Const(2)))), + ), + MemberOverload(overloads.ExtFormatString, + EvalCost(Scale(Target(), StringTraversalCostFactor)), + ), + + // O(n) containment + Overload(overloads.InList, EvalCost(Arg(1))), + + // Short-circuiting optional selection. The planner rewrites these calls into a branch which + // evaluates the alternative only when the receiver is empty, and which is not charged at + // runtime, so the model reports no cost for the dispatch itself. The alternative's cost is + // bounded by calculateArgCost. + // + // From version 1 the result is whichever of the two is present: or() returns one of the two + // optionals, and orValue() the receiver's value or the alternative. Before the revision these + // were unmodeled calls, which v0.32.0 estimated at 1 with no result size, although it charged + // them nothing. + MemberOverload(overloads.OptionalOrOptional, + EvalCost(Const(0)), + argumentsOnly(), + ResultSize(Union(Target(), Arg(0))), + BeforeVersion(1, ManualEstimator(estimateUnmodeledCall)), + ), + MemberOverload(overloads.OptionalOrValueValue, + EvalCost(Const(0)), + argumentsOnly(), + ResultSize(Union(ElemOf(Target()), Arg(0))), + BeforeVersion(1, ManualEstimator(estimateUnmodeledCall)), + ), + + // Creating, wrapping and unwrapping an optional. These cost what an unmodeled call costs; they + // are modeled from version 1 so that the wrapped value's size survives the round trip, with an + // optional sized as 1 plus the value it holds (see Optional). Without them an estimate dies at + // optional.of(x) and everything downstream of the optional is unbounded. + Overload(overloads.OptionalNone, + EvalCost(Const(1)), + ResultSize(Optional(Const(0))), + BeforeVersion(1, ResultSize(nil)), + ), + Overload(overloads.OptionalOf, + EvalCost(Const(1)), + ResultSize(Optional(Arg(0))), + BeforeVersion(1, ResultSize(nil)), + ), + // ofNonZeroValue is empty for a zero value, so it holds a value of at most the argument's size. + Overload(overloads.OptionalOfNonZeroValue, + EvalCost(Const(1)), + ResultSize(Optional(AtMost(Arg(0)))), + BeforeVersion(1, ResultSize(nil)), + ), + MemberOverload(overloads.OptionalValue, + EvalCost(Const(1)), + ResultSize(ElemOf(Target())), + BeforeVersion(1, ResultSize(nil)), + ), + + // O(min(m, n)) comparison / equality. Before version 1 the estimate keeps the v0.32.0 bounds of + // estimateComparisonV0; the charge is the same at every version. + Overload(overloads.LessString, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.GreaterString, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.LessEqualsString, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.GreaterEqualsString, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.LessBytes, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.GreaterBytes, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.LessEqualsBytes, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.GreaterEqualsBytes, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.Equals, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + Overload(overloads.NotEquals, + EvalCost(Scale(Min(Arg(0), Arg(1)), StringTraversalCostFactor)), + BeforeVersion(1, ManualEstimator(estimateComparisonV0)), + ), + + // O(m+n) string & bytes concatenation + Overload(overloads.AddString, + EvalCost(Scale(Sum(Arg(0), Arg(1)), StringTraversalCostFactor)), + ResultSize(Sum(Arg(0), Arg(1))), + ), + Overload(overloads.AddBytes, + EvalCost(Scale(Sum(Arg(0), Arg(1)), StringTraversalCostFactor)), + ResultSize(Sum(Arg(0), Arg(1))), + ), + + // O(1) list concatenation with size tracking + Overload(overloads.AddList, + EvalCost(Const(1)), + ResultSize(Sum(Arg(0), Arg(1))), + ), + + // O(nm) regex matches + Overload(overloads.Matches, + EvalCost(Mul( + Scale(Sum(Arg(0), Const(1)), StringTraversalCostFactor), + Scale(Arg(1), RegexStringLengthCostFactor), + )), + BeforeVersion(1, EvalCost(Mul( + roundScale(Sum(Arg(0), Const(1)), StringTraversalCostFactor), + roundScale(Arg(1), RegexStringLengthCostFactor), + ))), + ), + MemberOverload(overloads.MatchesString, + EvalCost(Mul( + Scale(Sum(Target(), Const(1)), StringTraversalCostFactor), + Scale(Arg(0), RegexStringLengthCostFactor), + )), + BeforeVersion(1, EvalCost(Mul( + roundScale(Sum(Target(), Const(1)), StringTraversalCostFactor), + roundScale(Arg(0), RegexStringLengthCostFactor), + ))), + ), + + // O(nm) substring contains + MemberOverload(overloads.ContainsString, + EvalCost(Mul( + Scale(Target(), StringTraversalCostFactor), + Scale(Arg(0), StringTraversalCostFactor), + )), + BeforeVersion(1, EvalCost(Mul( + roundScale(Target(), StringTraversalCostFactor), + roundScale(Arg(0), StringTraversalCostFactor), + ))), + ), + + // The arg cost for logical and conditional operators is special-cased for + // short-circuiting (see CalculateArgCost in estimator.go), so the arg cost is 0. + + // Logical short-circuiting operators + Overload(overloads.LogicalOr, EvalCost(Const(0)), argumentsOnly()), + Overload(overloads.LogicalAnd, EvalCost(Const(0)), argumentsOnly()), + + // Conditional operator + Overload(overloads.Conditional, + EvalCost(Const(0)), + argumentsOnly(), + ResultSize(Union(Arg(1), Arg(2))), + ), +} + +// roundScale scales e by f and rounds before any outer multiplication, reproducing the version 0 +// rounding of products of scaled factors. +func roundScale(e QuantityExpr, f float64) QuantityExpr { + s := Scale(e, f) + return Ranged(s, s) +} + +// StandardOverloadEstimators returns the map of FunctionEstimator instances for standard overloads. +func StandardOverloadEstimators() map[string]FunctionEstimator { + return StandardOverloadEstimatorsWithOptions() +} + +// StandardOverloadEstimatorsWithOptions returns the map of FunctionEstimator instances for standard +// overloads, configured by the supplied options. +func StandardOverloadEstimatorsWithOptions(opts ...ModelOption) map[string]FunctionEstimator { + estimators := make(map[string]FunctionEstimator, len(StandardOverloadModels)) + for _, m := range StandardOverloadModels { + estimators[m.ID] = m.FunctionEstimatorWithOptions(opts...) + } + return estimators +} + +// StandardOverloadTrackers returns the map of FunctionTracker instances for standard overloads. +func StandardOverloadTrackers() map[string]FunctionTracker { + return StandardOverloadTrackersWithOptions() +} + +// StandardOverloadTrackersWithOptions returns the map of FunctionTracker instances for standard +// overloads, configured by the supplied options. +// +// The models are tracked under the configured revision, so that each charge is computed by the same +// rules as its estimate. +func StandardOverloadTrackersWithOptions(opts ...ModelOption) map[string]FunctionTracker { + trackers := make(map[string]FunctionTracker, len(StandardOverloadModels)) + for _, m := range StandardOverloadModels { + trackers[m.ID] = m.FunctionTrackerWithOptions(opts...) + } + return trackers +} + +// estimateUnmodeledCall estimates a call as the estimator does a call it has no model for: a cost +// of 1, with no result size. +func estimateUnmodeledCall(Estimator, *AstNode, []AstNode) *CallEstimate { + return NewCallEstimate(CallCostEstimate, nil) +} + +// estimateComparisonV0 estimates a string, bytes or equality comparison as cel-go v0.32.0 did: +// ceil(0.1 * [m, min(lhs.Max, rhs.Max)]), where m is 1 when the smaller maximum is non-zero, and 0 +// otherwise. +// +// The lower bound is the charge of comparing any non-empty operands, whatever their minimum sizes, +// where the version 1 model reports ceil(0.1 * min(lhs.Min, rhs.Min)). The upper bound scales an +// unknown size like any other, where the version 1 model saturates it at math.MaxUint64. +func estimateComparisonV0(estimator Estimator, _ *AstNode, args []AstNode) *CallEstimate { + if len(args) != 2 { + return nil + } + smallestMax := min(EstimateSize(estimator, args[0]).Max, EstimateSize(estimator, args[1]).Max) + lo := uint64(0) + if smallestMax > 0 { + lo = 1 + } + return NewCallEstimate(CostEstimate{Min: lo, Max: smallestMax}.MultiplyByCostFactor(StringTraversalCostFactor), nil) +} diff --git a/vendor/cel.dev/cel-go/common/cost/strategy.go b/vendor/cel.dev/cel-go/common/cost/strategy.go new file mode 100644 index 00000000000..fb5b0f58aa9 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/strategy.go @@ -0,0 +1,29 @@ +// 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 cost + +import ( + "cel.dev/cel-go/common/types/ref" +) + +// SizingStrategy defines how sizes and container element sizes are estimated and tracked. +type SizingStrategy interface { + // EstimateSize computes the size estimate for an AST node during cost estimation. + // For lists and maps, the returned SizeEstimate can recursively include Elem and Key sizes. + EstimateSize(ctx EstimateContext, node AstNode) (SizeEstimate, bool) + + // TrackSize computes the actual runtime size of a value during cost tracking. + TrackSize(ctx TrackContext, value ref.Val) (uint64, bool) +} diff --git a/vendor/cel.dev/cel-go/common/cost/tracker.go b/vendor/cel.dev/cel-go/common/cost/tracker.go new file mode 100644 index 00000000000..3e72ba84119 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/tracker.go @@ -0,0 +1,297 @@ +// 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 cost + +import ( + "cel.dev/cel-go/common/types/ref" + "fmt" +) + +// WARNING: Any changes to cost calculations in this file require a corresponding change in estimator.go + +// ActualCostEstimator provides function call cost estimations at runtime. +// +// CallCost returns an estimated cost for the function overload invocation with the given args, or nil if it has no +// estimate to provide. CEL attempts to provide reasonable estimates for its standard function library, so CallCost +// should typically not need to provide an estimate for CELs standard function. +type ActualCostEstimator interface { + CallCost(function, overloadID string, args []ref.Val, result ref.Val) *uint64 +} + +// FunctionTracker computes the actual cost of evaluating the functions with the given arguments and result. +type FunctionTracker func(args []ref.Val, result ref.Val) *uint64 + +// Call represents an invocable function with a name and overload ID. +type Call interface { + // Function returns the name of the function being called. + Function() string + + // OverloadID returns the specific overload ID being invoked. + OverloadID() string +} + +// TrackerOption configures the behavior of CostTracker objects. +type TrackerOption interface { + applyTracker(*trackerBuilder) error +} + +type trackerOption func(*Tracker) error + +func (opt trackerOption) applyTracker(b *trackerBuilder) error { + return opt(b.tracker) +} + +type trackerBuilderOption func(*trackerBuilder) error + +func (opt trackerBuilderOption) applyTracker(b *trackerBuilder) error { + return opt(b) +} + +// trackerBuilder holds the configuration which matters only while NewTracker builds a Tracker. +// +// It is kept out of the Tracker, which is copied for every evaluation by Clone, so that the +// per-evaluation copy carries only what pricing a call reads. +type trackerBuilder struct { + tracker *Tracker + modelRules + overloadTrackers overloadTable[FunctionTracker] +} + +// TrackerLimit sets the runtime limit on the evaluation cost during execution and will terminate the expression +// evaluation if the limit is exceeded. +func TrackerLimit(limit uint64) TrackerOption { + return trackerOption(func(tracker *Tracker) error { + tracker.Limit = &limit + return nil + }) +} + +// TrackerPresenceTestHasCost determines whether presence testing has a cost of one or zero. +// Defaults to presence test has a cost of one. +func TrackerPresenceTestHasCost(hasCost bool) TrackerOption { + return trackerOption(func(tracker *Tracker) error { + tracker.presenceTestHasCost = hasCost + return nil + }) +} + +// TrackerLimitExceededHandler sets a custom handler invoked when the cost limit is exceeded. +func TrackerLimitExceededHandler(handler func()) TrackerOption { + return trackerOption(func(tracker *Tracker) error { + tracker.limitExceededHandler = handler + return nil + }) +} + +// OverloadTracker binds an overload ID to a runtime FunctionTracker implementation. +// +// OverloadTracker instances augment or override ActualCostEstimator decisions, allowing for +// versioned and/or optional cost tracking changes. +func OverloadTracker(overloadID string, fnTracker FunctionTracker) TrackerOption { + return trackerBuilderOption(func(b *trackerBuilder) error { + if fnTracker == nil { + return fmt.Errorf("OverloadTracker(%q) requires a FunctionTracker", overloadID) + } + b.overloadTrackers.mutable()[overloadID] = fnTracker + return nil + }) +} + +// LimitExceededError indicates that the actual cost limit was exceeded during evaluation. +type LimitExceededError struct { + Message string +} + +// Error returns the error message for LimitExceededError. +func (e LimitExceededError) Error() string { + return e.Message +} + +// Tracker represents the information needed for tracking runtime cost. +type Tracker struct { + Estimator ActualCostEstimator + overloadTrackers map[string]FunctionTracker + standardOverloadTrackers map[string]FunctionTracker + Limit *uint64 + limitExceededHandler func() + cost uint64 + presenceTestHasCost bool + modelVersion uint32 +} + +// NewTracker creates a new Tracker with a given estimator and a set of functional TrackerOption values. +// +// The Tracker's configuration is fixed once it is returned; only its accumulated cost changes. +func NewTracker(estimator ActualCostEstimator, opts ...TrackerOption) (*Tracker, error) { + b := &trackerBuilder{ + tracker: &Tracker{Estimator: estimator, presenceTestHasCost: true}, + modelRules: defaultModelRules(), + } + for _, opt := range opts { + if mo, ok := opt.(ModelOption); ok { + b.modelRules.override(mo) + } + } + for _, opt := range opts { + if err := opt.applyTracker(b); err != nil { + return nil, err + } + } + tracker := b.tracker + tracker.modelVersion = b.modelVersion + tracker.overloadTrackers = b.overloadTrackers.fns + // The package-level trackers are compiled under the default rules. + tracker.standardOverloadTrackers = stdOverloadTrackers + if !b.modelRules.isDefault() { + tracker.standardOverloadTrackers = StandardOverloadTrackersWithOptions(b.modelRules.modelOptions()...) + } + return tracker, nil +} + +// Clone makes a shallow copy of the tracker with no accumulated cost. +// The different clones can be used independently from each other: they share only configuration, +// which is not modified after NewTracker returns. +func (c *Tracker) Clone() (*Tracker, error) { + tracker := *c + tracker.cost = 0 + return &tracker, nil +} + +// ActualCost returns the runtime cost. +func (c *Tracker) ActualCost() uint64 { + return c.cost +} + +// PresenceTestHasCost returns whether presence testing has a cost. +func (c *Tracker) PresenceTestHasCost() bool { + return c.presenceTestHasCost +} + +// ModelVersion returns the cost model version configured on the tracker. +func (c *Tracker) ModelVersion() uint32 { + if c == nil { + return defaultModelVersion + } + return c.modelVersion +} + +// CreateList records list literal construction cost. +func (c *Tracker) CreateList(id int64, res ref.Val) { + c.cost = SafeAdd(c.cost, ListCreateBaseCost) + c.checkLimit() +} + +// CreateMap records map literal construction cost. +func (c *Tracker) CreateMap(id int64, res ref.Val) { + c.cost = SafeAdd(c.cost, MapCreateBaseCost) + c.checkLimit() +} + +// CreateStruct records struct/object construction cost. +func (c *Tracker) CreateStruct(id int64, res ref.Val) { + c.cost = SafeAdd(c.cost, StructCreateBaseCost) + c.checkLimit() +} + +// EvalAttribute records attribute resolution cost (ident / select). +func (c *Tracker) EvalAttribute(id int64, isTestOnly bool, res ref.Val) { + if !isTestOnly || c.presenceTestHasCost { + c.cost = SafeAdd(c.cost, SelectAndIdentCost) + c.checkLimit() + } +} + +// Qualify records qualifier cost. +func (c *Tracker) Qualify(id int64) { + c.cost = SafeAdd(c.cost, 1) + c.checkLimit() +} + +func (c *Tracker) recordCallCost(call Call, args []ref.Val, result ref.Val) { + c.cost = SafeAdd(c.cost, c.CostCall(call, args, result)) + c.checkLimit() +} + +// EvalZeroArity records the cost for a 0-arity call expression. +func (c *Tracker) EvalZeroArity(vars any, id int64, call Call, result ref.Val) { + c.recordCallCost(call, nil, result) +} + +// EvalUnary records the cost for a unary call expression. +func (c *Tracker) EvalUnary(vars any, id int64, call Call, arg ref.Val, result ref.Val) { + var buf [1]ref.Val + buf[0] = arg + c.recordCallCost(call, buf[:], result) +} + +// EvalBinary records the cost for a binary call expression. +func (c *Tracker) EvalBinary(vars any, id int64, call Call, lhs, rhs ref.Val, result ref.Val) { + var buf [2]ref.Val + buf[0] = lhs + buf[1] = rhs + c.recordCallCost(call, buf[:], result) +} + +// EvalVarArgs records the cost for a variadic call expression. +func (c *Tracker) EvalVarArgs(vars any, id int64, call Call, args []ref.Val, result ref.Val) { + c.recordCallCost(call, args, result) +} + +func (c *Tracker) checkLimit() { + if c.Limit != nil && c.cost > *c.Limit { + if c.limitExceededHandler != nil { + c.limitExceededHandler() + } + panic(LimitExceededError{Message: "operation cancelled: actual cost limit exceeded"}) + } +} + +// CostCall calculates the runtime cost for a function call. +func (c *Tracker) CostCall(call Call, args []ref.Val, result ref.Val) uint64 { + var total uint64 + if tracker, found := c.overloadTrackers[call.OverloadID()]; found { + callCost := tracker(args, result) + if callCost != nil { + total = SafeAdd(total, *callCost) + return total + } + } + if c.Estimator != nil { + callCost := c.Estimator.CallCost(call.Function(), call.OverloadID(), args, result) + if callCost != nil { + total = SafeAdd(total, *callCost) + return total + } + } + if tracker, found := c.standardOverloadTrackers[call.OverloadID()]; found { + callCost := tracker(args, result) + if callCost != nil { + total = SafeAdd(total, *callCost) + return total + } + } + // The following operations are assumed to have O(1) complexity. + // - AddList due to the implementation. Index lookup can be O(c) the + // number of concatenated lists, but we don't track that in cost calculations. + // - Conversions, since none perform a traversal of a type of unbound length. + // - Computing the size of strings, byte sequences, lists and maps. + // - Logical operations and all operators on fixed width scalars (comparisons, equality) + // - Any functions that don't have a declared cost either here or in provided ActualCostEstimator. + return SafeAdd(total, 1) +} + +var ( + stdOverloadTrackers = StandardOverloadTrackers() +) diff --git a/vendor/cel.dev/cel-go/common/cost/validate.go b/vendor/cel.dev/cel-go/common/cost/validate.go new file mode 100644 index 00000000000..f9ecd3f5d16 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/validate.go @@ -0,0 +1,275 @@ +// 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 cost + +import ( + "errors" + "fmt" + "math" +) + +// Validate reports whether the model is well-formed. +// +// A model is checked when it is registered, rather than when it is evaluated, because neither of +// the two evaluation paths can report a problem: an estimator has nowhere to return an error, and +// a tracker which cannot answer an expression would have to choose between charging nothing and +// charging the maximum, one of which lets an expression through and the other of which fails every +// evaluation that sets a cost limit. +// +// The checks apply at every model version, because every model they reject is mispriced at every +// version. They are: +// - the overload has an ID, and an evaluation cost expression; +// - every Arg and IntArg index is non-negative; +// - every element or key projection is applied to an expression which denotes a value; +// - every ElemTotal, KeyTotal and Length is applied to Arg, Target or Result; +// - a global overload does not reference the receiver; +// - every Scale factor is finite and positive; +// - no Ranged has constant bounds in the wrong order; +// - the result size expression does not reference Result(). +// +// Projections read the value they project from, so ElemOf, KeyOf and the ArgElem, ArgKey, +// TargetElem and TargetKey shorthands are defined over Arg, Target, Result, and the ElemOf and +// KeyOf projections of those. A projection of a computed quantity, such as +// ElemOf(Sum(Arg(0), Arg(1))), has an estimate but no runtime counterpart, and is reported here. +// ElemTotal, KeyTotal and Length estimate a quantity from the size of a single value, so they are +// defined over Arg, Target and Result only. +func (m OverloadModel) Validate() error { + return m.validate(false) +} + +func (m OverloadModel) validate(inPrior bool) error { + if m.ID == "" { + return errors.New("overload model requires an ID") + } + if m.manual && m.manualEstimator { + return fmt.Errorf("overload %q cannot combine ManualCost with ManualEstimator", m.ID) + } + if m.manual && (m.Cost != nil || m.hasSize) { + return fmt.Errorf("overload %q cannot combine ManualCost with EvalCost or ResultSize", m.ID) + } + if m.manual && (m.estimator == nil || m.tracker == nil) { + return fmt.Errorf("overload %q ManualCost requires both a FunctionEstimator and a FunctionTracker", m.ID) + } + if m.manualEstimator && m.hasSize { + return fmt.Errorf("overload %q cannot combine ManualEstimator with ResultSize", m.ID) + } + if m.manualEstimator && m.estimator == nil { + return fmt.Errorf("overload %q ManualEstimator requires a FunctionEstimator", m.ID) + } + if !m.manual && m.Cost == nil && !(inPrior && (m.manualEstimator || m.hasSize)) { + return fmt.Errorf("overload %q requires an EvalCost expression or ManualCost", m.ID) + } + if m.Cost != nil { + if err := validateQuantityExpr(m.Cost); err != nil { + return fmt.Errorf("overload %q eval cost: %w", m.ID, err) + } + } + if m.Size != nil || (m.hasSize && !inPrior) { + if err := validateQuantityExpr(m.Size); err != nil { + return fmt.Errorf("overload %q result size: %w", m.ID, err) + } + } + if !m.IsMember && (hasTarget(m.Cost) || hasTarget(m.Size)) { + return fmt.Errorf("overload %q is global but its model references the receiver; use MemberOverload", m.ID) + } + if m.Size != nil && references(m.Size, func(e QuantityExpr) bool { _, ok := e.(resultExpr); return ok }) { + return fmt.Errorf("overload %q result size references Result()", m.ID) + } + return validatePriors(m) +} + +func validatePriors(m OverloadModel) error { + seen := make(map[uint32]struct{}, len(m.priors)) + for _, p := range m.priors { + if p.before == 0 { + return fmt.Errorf("overload %q: BeforeVersion requires a version >= 1, got 0", m.ID) + } + if _, dup := seen[p.before]; dup { + return fmt.Errorf("overload %q: duplicate BeforeVersion(%d)", m.ID, p.before) + } + seen[p.before] = struct{}{} + if err := p.model.validate(true); err != nil { + return fmt.Errorf("overload %q before version %d: %w", m.ID, p.before, err) + } + resolved := m.resolvePrior(p.model) + if !resolved.manual && resolved.Cost == nil { + return fmt.Errorf("overload %q before version %d: overload %q requires an EvalCost expression or ManualCost", m.ID, p.before, m.ID) + } + } + return nil +} + +// references reports whether pred holds for the expression or any of its operands. +func references(expr QuantityExpr, pred func(QuantityExpr) bool) bool { + if pred(expr) { + return true + } + _, operands := describeQuantityExpr(expr) + for _, operand := range operands { + if references(operand, pred) { + return true + } + } + return false +} + +// ValidateOverloadModels reports the first model which is not well-formed. +func ValidateOverloadModels(models ...OverloadModel) error { + for _, m := range models { + if err := m.Validate(); err != nil { + return err + } + } + return nil +} + +// validateQuantityExpr reports the first operand of an expression tree which cannot be evaluated. +// +// A nil operand is reported here so that it fails registration rather than panicking when an +// estimate or a charge is first computed. +func validateQuantityExpr(expr QuantityExpr) error { + if expr == nil { + return errors.New("nil quantity expression") + } + label, operands := describeQuantityExpr(expr) + switch e := expr.(type) { + case argExpr: + if e.index < 0 { + return fmt.Errorf("Arg index must be non-negative, got %d", e.index) + } + case intArgExpr: + if e.index < 0 { + return fmt.Errorf("IntArg index must be non-negative, got %d", e.index) + } + case scaleExpr: + if math.IsNaN(e.factor) || math.IsInf(e.factor, 0) || e.factor <= 0 { + return fmt.Errorf("Scale factor must be finite and positive, got %v", e.factor) + } + case rangedExpr: + lo, loConst := e.minExpr.(constExpr) + hi, hiConst := e.maxExpr.(constExpr) + if loConst && hiConst && lo.val > hi.val { + return fmt.Errorf("Ranged lower bound %d exceeds upper bound %d", lo.val, hi.val) + } + } + // A nil operand is left to the recursion below to report. + if _, isProjection := expr.(projection); isProjection && operands[0] != nil { + if _, ok := operands[0].(valueSource); !ok { + operandLabel, _ := describeQuantityExpr(operands[0]) + return fmt.Errorf( + "%s is not defined over %s: element and key projections apply to Arg, Target, Result, and ElemOf or KeyOf of those", + label, operandLabel) + } + } + if _, single := expr.(singleValueProjection); single { + switch operands[0].(type) { + case argExpr, targetExpr, resultExpr: + default: + operandLabel, _ := describeQuantityExpr(operands[0]) + return fmt.Errorf( + "%s is not defined over %s: it applies to Arg, Target and Result, whose size bounds it", + label, operandLabel) + } + } + for _, operand := range operands { + if err := validateQuantityExpr(operand); err != nil { + return fmt.Errorf("%s: %w", label, err) + } + } + return nil +} + +// projection marks the expressions which read the elements or keys of the value they are applied +// to, and so constrain what that operand may be. +type projection interface { + QuantityExpr + projects() +} + +func (elemExpr) projects() {} +func (keyExpr) projects() {} +func (elemTotalExpr) projects() {} +func (keyTotalExpr) projects() {} +func (lengthExpr) projects() {} + +// singleValueProjection marks the projections whose estimate is derived from the size of a single +// value, so the operand must denote exactly one value: a total or length of ElemOf(X) would range +// over every element of X, which no single size estimate bounds. +type singleValueProjection interface { + projection + singleValue() +} + +func (elemTotalExpr) singleValue() {} +func (keyTotalExpr) singleValue() {} +func (lengthExpr) singleValue() {} + +// describeQuantityExpr reports the constructor name of a quantity expression and its operands. +// +// Every expression type must be listed: an omission makes an expression's operands invisible to +// validation, and reports an unhelpful name in the error message. TestDescribeQuantityExpr covers +// each constructor. +func describeQuantityExpr(expr QuantityExpr) (string, []QuantityExpr) { + switch e := expr.(type) { + case constExpr: + return "Const", nil + case argExpr: + return "Arg", nil + case intArgExpr: + return "IntArg", nil + case targetExpr: + return "Target", nil + case intTargetExpr: + return "IntTarget", nil + case resultExpr: + return "Result", nil + case elemExpr: + return "ElemOf", []QuantityExpr{e.expr} + case keyExpr: + return "KeyOf", []QuantityExpr{e.expr} + case elemTotalExpr: + return "ElemTotal", []QuantityExpr{e.expr} + case keyTotalExpr: + return "KeyTotal", []QuantityExpr{e.expr} + case lengthExpr: + return "Length", []QuantityExpr{e.expr} + case addExpr: + return "Sum", e.terms + case subExpr: + return "Sub", []QuantityExpr{e.lhs, e.rhs} + case mulExpr: + return "Mul", e.terms + case scaleExpr: + return "Scale", []QuantityExpr{e.expr} + case minExpr: + return "Min", []QuantityExpr{e.lhs, e.rhs} + case maxExpr: + return "Max", []QuantityExpr{e.lhs, e.rhs} + case unionExpr: + return "Union", e.terms + case rangedExpr: + return "Ranged", []QuantityExpr{e.minExpr, e.maxExpr} + case atLeastOneExpr: + return "AtLeastOneQuantity", []QuantityExpr{e.expr} + case listExpr: + return "List", []QuantityExpr{e.lenExpr, e.elemExpr} + case mapExpr: + return "Map", []QuantityExpr{e.sizeExpr, e.keyExpr, e.valExpr} + case optionalExpr: + return "Optional", []QuantityExpr{e.valueExpr} + default: + return fmt.Sprintf("%T", expr), nil + } +} diff --git a/vendor/cel.dev/cel-go/common/cost/version.go b/vendor/cel.dev/cel-go/common/cost/version.go new file mode 100644 index 00000000000..5b4b45bd7a9 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/cost/version.go @@ -0,0 +1,299 @@ +// 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 cost + +import ( + "maps" + "math" + "slices" + "sync" +) + +const defaultModelVersion uint32 = math.MaxUint32 + +// ModelOption configures how OverloadModels, cost estimators, and cost trackers are built and +// evaluated. +// +// A ModelOption implements both Option (for Cost) and TrackerOption (for NewTracker), and may also +// be passed when compiling models directly via FunctionEstimatorWithOptions, +// FunctionTrackerWithOptions, StandardOverloadEstimatorsWithOptions, and +// StandardOverloadTrackersWithOptions. +type ModelOption func(*modelOptions) + +// modelRules are the sizing strategy and model revision under which an estimator or a tracker +// prices calls. Both are fixed once the estimator or tracker has been configured. +type modelRules struct { + sizingStrategy SizingStrategy + modelVersion uint32 +} + +func defaultModelRules() modelRules { + return modelRules{sizingStrategy: defaultSizing, modelVersion: defaultModelVersion} +} + +// isDefault reports whether the rules are those the package-level and per-model-set caches are +// compiled under, so that the cached functions may be shared rather than recompiled. +func (r modelRules) isDefault() bool { + return r.sizingStrategy == defaultSizing && r.modelVersion == defaultModelVersion +} + +func (r modelRules) modelOptions() []ModelOption { + return []ModelOption{WithSizingStrategy(r.sizingStrategy), ModelVersion(r.modelVersion)} +} + +// override replaces the rules the option sets, leaving the others in place. +func (r *modelRules) override(opt ModelOption) { + o := newModelOptions(opt) + if o.hasStrategy { + r.sizingStrategy = o.strategy + } + if o.hasVersion { + r.modelVersion = o.version + } +} + +// compiledModelSet is the immutable product of a Model option: the models, the result of +// validating them, and, built on first use and shared by every estimator and tracker configured +// with the option, the functions the models compile to under the default rules. +type compiledModelSet struct { + models []OverloadModel + err error + estOnce sync.Once + defaultEstimators map[string]FunctionEstimator + trkOnce sync.Once + defaultTrackers map[string]FunctionTracker +} + +func newCompiledModelSet(models []OverloadModel) *compiledModelSet { + return &compiledModelSet{ + models: models, + err: ValidateOverloadModels(models...), + } +} + +func (cm *compiledModelSet) estimators() map[string]FunctionEstimator { + cm.estOnce.Do(func() { + var t overloadTable[FunctionEstimator] + t.compile(cm.models, estimatorCompiler()) + cm.defaultEstimators = t.fns + }) + return cm.defaultEstimators +} + +func (cm *compiledModelSet) trackers() map[string]FunctionTracker { + cm.trkOnce.Do(func() { + var t overloadTable[FunctionTracker] + t.compile(cm.models, trackerCompiler()) + cm.defaultTrackers = t.fns + }) + return cm.defaultTrackers +} + +// overloadTable maps overload IDs to the FunctionEstimators or FunctionTrackers which price them. +// +// Options populate a table while an estimator or tracker is configured; it is read-only after. +// A table configured with a single model set under the default rules refers to the set's cached +// functions rather than copying them, and is copied before its first write. +type overloadTable[F any] struct { + fns map[string]F + shared bool +} + +func (t *overloadTable[F]) mutable() map[string]F { + if t.shared { + t.fns, t.shared = maps.Clone(t.fns), false + } else if t.fns == nil { + t.fns = map[string]F{} + } + return t.fns +} + +// compile binds each model's function in order, so a later model replaces an earlier one with the +// same ID. +func (t *overloadTable[F]) compile(models []OverloadModel, compile func(OverloadModel) F) { + fns := t.mutable() + for _, m := range models { + fns[m.ID] = compile(m) + } +} + +// addModelSet binds the functions the models of cm compile to under the given rules. +func (t *overloadTable[F]) addModelSet(cm *compiledModelSet, rules modelRules, + cached func() map[string]F, compile func(...ModelOption) func(OverloadModel) F) error { + if cm.err != nil { + return cm.err + } + if !rules.isDefault() { + t.compile(cm.models, compile(rules.modelOptions()...)) + return nil + } + if t.fns == nil { + t.fns, t.shared = cached(), true + return nil + } + t.compile(cm.models, compile()) + return nil +} + +func estimatorCompiler(opts ...ModelOption) func(OverloadModel) FunctionEstimator { + return func(m OverloadModel) FunctionEstimator { + return m.FunctionEstimatorWithOptions(opts...) + } +} + +func trackerCompiler(opts ...ModelOption) func(OverloadModel) FunctionTracker { + return func(m OverloadModel) FunctionTracker { + return m.FunctionTrackerWithOptions(opts...) + } +} + +// modelOptions is the resolved configuration for compiling an OverloadModel or configuring an +// estimator or tracker. +type modelOptions struct { + strategy SizingStrategy + hasStrategy bool + version uint32 + hasVersion bool + modelSets []*compiledModelSet +} + +// A ModelOption is applied in two passes by Cost and NewTracker. The first, over every ModelOption +// before any other option, settles the rules, so that models are compiled under the final rules +// whatever the option order. The second registers the model sets in order with the other options, +// so that a later registration of an overload ID overrides an earlier one. + +func (opt ModelOption) applyCoster(c *coster) error { + for _, cm := range newModelOptions(opt).modelSets { + if err := c.overloadEstimators.addModelSet(cm, c.modelRules, cm.estimators, estimatorCompiler); err != nil { + return err + } + } + return nil +} + +func (opt ModelOption) applyTracker(b *trackerBuilder) error { + for _, cm := range newModelOptions(opt).modelSets { + if err := b.overloadTrackers.addModelSet(cm, b.modelRules, cm.trackers, trackerCompiler); err != nil { + return err + } + } + return nil +} + +// newModelOptions resolves the supplied options over the defaults. +func newModelOptions(opts ...ModelOption) *modelOptions { + resolved := &modelOptions{ + strategy: defaultSizing, + version: defaultModelVersion, + } + for _, opt := range opts { + opt(resolved) + } + return resolved +} + +// Model registers one or more OverloadModels for both cost estimation and runtime cost tracking. +// +// Models are validated when the option is applied to Cost or NewTracker and resolved after all +// options have run, so they observe the WithSizingStrategy and ModelVersion configured on the call +// regardless of option order. +func Model(models ...OverloadModel) ModelOption { + cm := newCompiledModelSet(slices.Clone(models)) + return func(o *modelOptions) { + o.modelSets = append(o.modelSets, cm) + } +} + +// WithSizingStrategy sets the SizingStrategy used to resolve sizes during both cost estimation and +// runtime cost tracking. +// +// A nil strategy is ignored, leaving the default in place. +func WithSizingStrategy(strategy SizingStrategy) ModelOption { + return func(o *modelOptions) { + o.hasStrategy = true + if strategy != nil { + o.strategy = strategy + } + } +} + +// ModelVersion pins the revision of the cost model's estimation and runtime tracking rules. +// +// If this option is not set, the latest cost model version is used. Revisions exist so that +// corrections to the model can land without silently moving numbers that existing deployments +// budget against. A caller holding estimates recorded under an earlier release can pin to the +// revision those estimates were produced under, review the delta at leisure, and adopt the +// correction deliberately. +// +// Revisions apply to both estimation and runtime tracking, so that an estimate and the charge it +// predicts are always computed by the same rules. Pinning a version therefore holds both stable. +// +// Caveat: Pinning to version 0 preserves legacy behavior from v0.32.0 and earlier, which contains +// known soundness gaps where static estimates can underestimate actual runtime cost (for example, +// when a caller's Estimator overrides computed loop variable sizes via bare "@items" paths, when +// selecting or indexing into computed containers, or when evaluating non-literal integer bounds). +// Callers are strongly encouraged to switch to ModelVersion(1) or higher (or leave the version +// unset to use the latest revision) for improved soundness between estimated bounds and tracked +// runtime cost. +// +// Version 1: +// - Min() is computed as the exact minimum of its operands' intervals, where v0.32.0 reported a +// lower bound of 1 for any interval whose upper bound is non-zero. The change moves only the +// lower bound of an estimate, never a charge, so it applies at every revision, including when +// pinned to version 0. +// - A value inside an optional keeps its size. optional.of, optional.ofNonZeroValue and value() +// report the size they wrap or unwrap, the two `or` forms report the union of the receiver and +// the alternative, and the optional index forms record the same `@items` and `@values` field +// path their non-optional counterparts do. +// - Optional field and index qualifiers (`m.?k`, `l[?0]`) charge 1 at runtime even when the +// field or index is absent, matching static estimation and non-optional qualification. +// - Selecting or indexing into a computed expression (such as `[1, 2][0]` or `{"a": 1}.a`) +// charges the attribute traversal cost of 1 during estimation to match runtime tracking. +// - Comprehension loop variables over computed (pathless) ranges retain their propagated element +// or key sizes rather than recording bare `@items`, `@indices`, `@keys`, or `@values` paths +// that a caller's Estimator could override with a smaller bound. +// - The default sizing strategy skips `@items`, `@keys`, and `@values` child size hints when a +// container's maximum size is 0. +// - Extension estimators in ext/lists (distinct, sort, sortBy, slice, range, flatten), +// ext/strings (join, split, substring, replace), and ext/regex (extract, extractAll, replace) +// align their Min/Max cost and result-size bounds with their runtime cost trackers. +// - Scale applies its factor as the simplest rational with the same float64 value (0.1 is 1/10) +// and computes the product exactly; it keeps an unbounded quantity unbounded; and when a model +// is compiled, factors are moved outward so that each product is rounded once. The standard +// contains and matches costs become ⌈0.01·m·n⌉ and ⌈0.025·(m+1)·n⌉. +// - Ranged never reports a lower bound above its upper bound. +// - Combinators keep an unknown element or key size unknown, treating constants and integer +// values as scalars; Sub carries only its minuend's metadata. +// - The result size is estimated first and is available to the cost through Result(). +// - A non-literal IntArg or IntTarget estimates as unbounded rather than as its default. +// - List and Map report the size the sizing strategy assigns to the container they describe. +// Under the aggregate strategy, a comprehension iterates once per element of its range rather +// than once per unit of content size, and sizes its result as an aggregate container. +// - Runtime tracking honors the revision, as estimation does. +// - Every ext overload with a cost estimator is priced by a model, which charges by Length what +// its tracker charged and states result sizes in the units of the sizing strategy; flatten(depth) +// follows the strategy in its estimator. An Arg or Target typed as a value without elements, +// such as a string, carries no unknown element size. +// - The value bound by cel.bind counts toward the minimum cost only when the result must read +// it, since the binding is lazy. +// - A list or map literal's minimum size counts only its required elements or entries, and an +// optional element written as optional.none() or optional.of(x) contributes nothing or the +// size of x. +func ModelVersion(version uint32) ModelOption { + return func(o *modelOptions) { + o.hasVersion = true + o.version = version + } +} diff --git a/vendor/github.com/google/cel-go/common/debug/BUILD.bazel b/vendor/cel.dev/cel-go/common/debug/BUILD.bazel similarity index 88% rename from vendor/github.com/google/cel-go/common/debug/BUILD.bazel rename to vendor/cel.dev/cel-go/common/debug/BUILD.bazel index 724ed340458..07fed427158 100644 --- a/vendor/github.com/google/cel-go/common/debug/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/debug/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "debug.go", ], - importpath = "github.com/google/cel-go/common/debug", + importpath = "cel.dev/cel-go/common/debug", deps = [ "//common:go_default_library", "//common/ast:go_default_library", diff --git a/vendor/github.com/google/cel-go/common/debug/debug.go b/vendor/cel.dev/cel-go/common/debug/debug.go similarity index 98% rename from vendor/github.com/google/cel-go/common/debug/debug.go rename to vendor/cel.dev/cel-go/common/debug/debug.go index fbc847f0c14..67593c9968b 100644 --- a/vendor/github.com/google/cel-go/common/debug/debug.go +++ b/vendor/cel.dev/cel-go/common/debug/debug.go @@ -22,9 +22,9 @@ import ( "strconv" "strings" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // Adorner returns debug metadata that will be tacked on to the string diff --git a/vendor/github.com/google/cel-go/common/decls/BUILD.bazel b/vendor/cel.dev/cel-go/common/decls/BUILD.bazel similarity index 95% rename from vendor/github.com/google/cel-go/common/decls/BUILD.bazel rename to vendor/cel.dev/cel-go/common/decls/BUILD.bazel index bd3f9ae70e1..85c7d5c41df 100644 --- a/vendor/github.com/google/cel-go/common/decls/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/decls/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "decls.go", ], - importpath = "github.com/google/cel-go/common/decls", + importpath = "cel.dev/cel-go/common/decls", deps = [ "//checker/decls:go_default_library", "//common:go_default_library", diff --git a/vendor/github.com/google/cel-go/common/decls/decls.go b/vendor/cel.dev/cel-go/common/decls/decls.go similarity index 81% rename from vendor/github.com/google/cel-go/common/decls/decls.go rename to vendor/cel.dev/cel-go/common/decls/decls.go index cd4d3a56599..f46e9750726 100644 --- a/vendor/github.com/google/cel-go/common/decls/decls.go +++ b/vendor/cel.dev/cel-go/common/decls/decls.go @@ -16,15 +16,16 @@ package decls import ( + "context" "fmt" "strings" - chkdecls "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + chkdecls "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -77,6 +78,9 @@ type FunctionDecl struct { // overloadOrdinals indicates the order in which the overload was declared. overloadOrdinals []string + + // overloadDecls caches the slice of overloads in declaration order. + overloadDecls []*OverloadDecl } type declarationState int @@ -150,7 +154,8 @@ func (f *FunctionDecl) Merge(other *FunctionDecl) (*FunctionDecl, error) { name: f.Name(), overloads: make(map[string]*OverloadDecl, len(f.overloads)), singleton: f.singleton, - overloadOrdinals: make([]string, len(f.overloads)), + overloadOrdinals: make([]string, len(f.overloadOrdinals)), + overloadDecls: make([]*OverloadDecl, len(f.overloadDecls)), // if one function is expecting type-guards and the other is not, then they // must not be disabled. disableTypeGuards: f.disableTypeGuards && other.disableTypeGuards, @@ -169,6 +174,7 @@ func (f *FunctionDecl) Merge(other *FunctionDecl) (*FunctionDecl, error) { } // baseline copy of the overloads and their ordinals copy(merged.overloadOrdinals, f.overloadOrdinals) + copy(merged.overloadDecls, f.overloadDecls) for oID, o := range f.overloads { merged.overloads[oID] = o } @@ -231,11 +237,13 @@ func (f *FunctionDecl) Subset(selector OverloadSelector) *FunctionDecl { } overloads := make(map[string]*OverloadDecl) overloadOrdinals := make([]string, 0, len(f.overloadOrdinals)) + overloadDecls := make([]*OverloadDecl, 0, len(f.overloadDecls)) for _, oID := range f.overloadOrdinals { overload := f.overloads[oID] if selector(overload) { overloads[oID] = overload overloadOrdinals = append(overloadOrdinals, oID) + overloadDecls = append(overloadDecls, overload) } } if len(overloads) == 0 { @@ -249,6 +257,7 @@ func (f *FunctionDecl) Subset(selector OverloadSelector) *FunctionDecl { disableTypeGuards: f.disableTypeGuards, state: f.state, overloadOrdinals: overloadOrdinals, + overloadDecls: overloadDecls, } return subset } @@ -272,6 +281,12 @@ func (f *FunctionDecl) AddOverload(overload *OverloadDecl) error { // Allow redefinition of an overload implementation so long as the signatures match. if overload.HasBinding() { f.overloads[oID] = overload + for i, decl := range f.overloadDecls { + if decl.ID() == oID { + f.overloadDecls[i] = overload + break + } + } } // Allow redefinition of the doc string. if len(overload.doc) != 0 && o.doc != overload.doc { @@ -287,20 +302,16 @@ func (f *FunctionDecl) AddOverload(overload *OverloadDecl) error { } f.overloadOrdinals = append(f.overloadOrdinals, overload.ID()) f.overloads[overload.ID()] = overload + f.overloadDecls = append(f.overloadDecls, overload) return nil } // OverloadDecls returns the overload declarations in the order in which they were declared. func (f *FunctionDecl) OverloadDecls() []*OverloadDecl { - var emptySet []*OverloadDecl if f == nil { - return emptySet + return nil } - overloads := make([]*OverloadDecl, 0, len(f.overloads)) - for _, oID := range f.overloadOrdinals { - overloads = append(overloads, f.overloads[oID]) - } - return overloads + return f.overloadDecls } // HasSingletonBinding indicates whether the function has a singleton binding definition. @@ -311,13 +322,34 @@ func (f *FunctionDecl) HasSingletonBinding() bool { return f.singleton != nil } +// IsAsync returns true if the function has an async binding. +func (f *FunctionDecl) IsAsync() bool { + if f == nil { + return false + } + if f.singleton != nil && f.singleton.Async != nil { + return true + } + for _, oID := range f.overloadOrdinals { + o := f.overloads[oID] + if o.IsAsync() { + return true + } + } + return false +} + // HasLateBinding returns true if the function has late bindings. A function cannot mix late bindings with other bindings. func (f *FunctionDecl) HasLateBinding() bool { if f == nil { return false } + if f.singleton != nil && f.singleton.LateBound { + return true + } for _, oID := range f.overloadOrdinals { - if f.overloads[oID].HasLateBinding() { + o := f.overloads[oID] + if o.HasLateBinding() { return true } } @@ -335,33 +367,34 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { hasLateBinding := false for _, oID := range f.overloadOrdinals { o := f.overloads[oID] - hasLateBinding = hasLateBinding || o.HasLateBinding() - if o.HasBinding() { - overload := &functions.Overload{ - Operator: o.ID(), - Unary: o.guardedUnaryOp(f.Name(), f.disableTypeGuards), - Binary: o.guardedBinaryOp(f.Name(), f.disableTypeGuards), - Function: o.guardedFunctionOp(f.Name(), f.disableTypeGuards), - OperandTrait: o.OperandTrait(), - NonStrict: o.IsNonStrict(), - } - overloads = append(overloads, overload) - nonStrict = nonStrict || o.IsNonStrict() + // An overload is either bound at declaration time or late-bound from the activation. + // The two are mutually exclusive, so each is configured independently. + switch { + case o.HasLateBinding(): + hasLateBinding = true + overloads = append(overloads, o.lateBoundBinding(f.disableTypeGuards)) + case o.HasBinding(): + overloads = append(overloads, o.binding(f.Name(), f.disableTypeGuards)) + default: + continue } + nonStrict = nonStrict || o.IsNonStrict() } if f.singleton != nil { - if len(overloads) != 0 { - return nil, fmt.Errorf("singleton function incompatible with specialized overloads: %s", f.Name()) - } if hasLateBinding { return nil, fmt.Errorf("singleton function incompatible with late bindings: %s", f.Name()) } + if len(overloads) != 0 { + return nil, fmt.Errorf("singleton function incompatible with specialized overloads: %s", f.Name()) + } overloads = []*functions.Overload{ { Operator: f.Name(), Unary: f.singleton.Unary, Binary: f.singleton.Binary, Function: f.singleton.Function, + Async: f.singleton.Async, + LateBound: f.singleton.LateBound, OperandTrait: f.singleton.OperandTrait, }, } @@ -375,13 +408,19 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { if overloads[0].Operator == f.Name() { return overloads, nil } + named := *overloads[0] + named.Operator = f.Name() + return append(overloads, &named), nil + } + if hasLateBinding { + // Late-bound implementations are resolved from the activation by function name, so the + // top-level entry dispatches over the declared overloads to determine which signature + // the runtime arguments satisfy. return append(overloads, &functions.Overload{ - Operator: f.Name(), - Unary: overloads[0].Unary, - Binary: overloads[0].Binary, - Function: overloads[0].Function, - NonStrict: overloads[0].NonStrict, - OperandTrait: overloads[0].OperandTrait, + Operator: f.Name(), + LateBound: true, + LateBoundDispatch: f.lateBoundDispatcher(), + NonStrict: nonStrict, }), nil } // All of the defined overloads are wrapped into a top-level function which @@ -538,6 +577,30 @@ func SingletonFunctionBinding(fn functions.FunctionOp, traits ...int) FunctionOp } } +// SingletonAsyncBinding creates a singleton async function definition to be used with all function overloads. +// The provided function is called in its own goroutine with the provided context. The function should +// block until the result is available, and the framework manages goroutine and channel lifecycle. +// +// Note, this approach works well if operand is expected to have a specific trait which it implements, +// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings. +func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt { + trait := 0 + for _, t := range traits { + trait = trait | t + } + return func(f *FunctionDecl) (*FunctionDecl, error) { + if f.singleton != nil { + return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name()) + } + f.singleton = &functions.Overload{ + Operator: f.Name(), + Async: wrapAsyncOp(fn), + OperandTrait: trait, + } + return f, nil + } +} + // Overload defines a new global overload with an overload id, argument types, and result type. Through the // use of OverloadOpt options, the overload may also be configured with a binding, an operand trait, and to // be non-strict. @@ -622,6 +685,8 @@ type OverloadDecl struct { binaryOp functions.BinaryOp // functionOp is a catch-all for zero-arity and three-plus arity functions. functionOp functions.FunctionOp + // asyncOp is an asynchronous function binding that returns a channel. + asyncOp functions.AsyncOp } // Examples returns a list of string examples for the overload. @@ -672,7 +737,15 @@ func (o *OverloadDecl) IsNonStrict() bool { return o.nonStrict } -// HasLateBinding returns whether the overload has a binding which is not known at compile time. +// IsAsync returns whether the overload has an asynchronous binding. +func (o *OverloadDecl) IsAsync() bool { + if o == nil { + return false + } + return o.asyncOp != nil +} + +// HasLateBinding returns whether the overload has a late binding which is resolved at runtime. func (o *OverloadDecl) HasLateBinding() bool { if o == nil { return false @@ -750,7 +823,7 @@ func (o *OverloadDecl) SignatureOverlaps(other *OverloadDecl) bool { // HasBinding indicates whether the overload already has a definition. func (o *OverloadDecl) HasBinding() bool { - return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil) + return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil || o.asyncOp != nil) } // guardedUnaryOp creates an invocation guard around the provided unary operator, if one is defined. @@ -792,6 +865,83 @@ func (o *OverloadDecl) guardedFunctionOp(funcName string, disableTypeGuards bool } } +// guardedAsyncOp creates an invocation guard around the provided async function binding, if one is provided. +func (o *OverloadDecl) guardedAsyncOp(funcName string, disableTypeGuards bool) functions.AsyncOp { + if o.asyncOp == nil { + return nil + } + return func(ctx context.Context, args ...ref.Val) <-chan ref.Val { + if !o.matchesRuntimeSignature(disableTypeGuards, args...) { + ch := make(chan ref.Val, 1) + ch <- MaybeNoSuchOverload(funcName, args...) + close(ch) + return ch + } + return o.asyncOp(ctx, args...) + } +} + +// binding returns the runtime binding for an overload whose implementation is provided at +// declaration time. +// +// Each implementation is wrapped in an invocation guard which ensures runtime type agreement +// between the overload signature and the argument values. +func (o *OverloadDecl) binding(funcName string, disableTypeGuards bool) *functions.Overload { + return &functions.Overload{ + Operator: o.ID(), + Unary: o.guardedUnaryOp(funcName, disableTypeGuards), + Binary: o.guardedBinaryOp(funcName, disableTypeGuards), + Function: o.guardedFunctionOp(funcName, disableTypeGuards), + Async: o.guardedAsyncOp(funcName, disableTypeGuards), + OperandTrait: o.OperandTrait(), + NonStrict: o.IsNonStrict(), + } +} + +// lateBoundBinding returns the runtime binding for an overload whose implementation is supplied +// by the activation at evaluation time. +// +// There is no implementation to wrap in an invocation guard, so the signature check is instead +// exposed to the interpreter which applies it to the implementation it resolves. +func (o *OverloadDecl) lateBoundBinding(disableTypeGuards bool) *functions.Overload { + return &functions.Overload{ + Operator: o.ID(), + LateBound: true, + LateBoundDispatch: o.lateBoundDispatcher(disableTypeGuards), + OperandTrait: o.OperandTrait(), + NonStrict: o.IsNonStrict(), + } +} + +// lateBoundDispatcher creates the runtime signature check for a late-bound overload. +func (o *OverloadDecl) lateBoundDispatcher(disableTypeGuards bool) functions.LateBoundDispatcher { + return func(memberStyle bool, args ...ref.Val) (string, bool) { + if memberStyle != o.IsMemberFunction() || !o.matchesRuntimeSignature(disableTypeGuards, args...) { + return "", false + } + return o.ID(), true + } +} + +// lateBoundDispatcher creates a runtime dispatcher over the late-bound overloads of the function. +// +// This mirrors the dynamic dispatch performed for functions with multiple eager overloads: the +// first overload whose receiver style and signature agree with the runtime arguments wins. +func (f *FunctionDecl) lateBoundDispatcher() functions.LateBoundDispatcher { + return func(memberStyle bool, args ...ref.Val) (string, bool) { + for _, oID := range f.overloadOrdinals { + o := f.overloads[oID] + if !o.HasLateBinding() || memberStyle != o.IsMemberFunction() { + continue + } + if o.matchesRuntimeSignature(f.disableTypeGuards, args...) { + return o.ID(), true + } + } + return "", false + } +} + // matchesRuntimeUnarySignature indicates whether the argument type is runtime assiganble to the overload's expected argument. func (o *OverloadDecl) matchesRuntimeUnarySignature(disableTypeGuards bool, arg ref.Val) bool { return matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[0], arg) && @@ -822,13 +972,10 @@ func (o *OverloadDecl) matchesRuntimeSignature(disableTypeGuards bool, args ...r } func matchRuntimeArgType(nonStrict, disableTypeGuards bool, argType *types.Type, arg ref.Val) bool { - if nonStrict && (disableTypeGuards || types.IsUnknownOrError(arg)) { + if disableTypeGuards || argType.IsAssignableRuntimeType(arg) { return true } - if types.IsUnknownOrError(arg) { - return false - } - return disableTypeGuards || argType.IsAssignableRuntimeType(arg) + return nonStrict && types.IsUnknownOrError(arg) } func matchOperandTrait(trait int, arg ref.Val) bool { @@ -897,8 +1044,45 @@ func FunctionBinding(binding functions.FunctionOp) OverloadOpt { } } +// AsyncBinding provides the implementation of an asynchronous overload. The provided function +// is called in its own goroutine with the provided context. The function should block until +// the result is available, and the framework manages goroutine and channel lifecycle. +// +// This follows the same pattern used by gRPC-Go and other major Go frameworks where user +// code is synchronous and the framework manages concurrency. +func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt { + return func(o *OverloadDecl) (*OverloadDecl, error) { + if o.HasBinding() { + return nil, fmt.Errorf("overload already has a binding: %s", o.ID()) + } + if o.hasLateBinding { + return nil, fmt.Errorf("overload already has a late binding: %s", o.ID()) + } + o.asyncOp = wrapAsyncOp(fn) + return o, nil + } +} + +// wrapAsyncOp adapts a blocking function into the channel-based AsyncOp used internally. +// +// The blocking function is invoked synchronously and its result delivered on a buffered channel. +// The interpreter always invokes an AsyncOp from a dedicated goroutine, so running the blocking +// call inline here keeps the framework to a single goroutine per async call rather than spawning +// an additional one to bridge blocking-to-channel. +func wrapAsyncOp(fn functions.BlockingAsyncOp) functions.AsyncOp { + return func(ctx context.Context, args ...ref.Val) <-chan ref.Val { + ch := make(chan ref.Val, 1) + ch <- fn(ctx, args...) + close(ch) + return ch + } +} + // LateFunctionBinding indicates that the function has a binding which is not known at compile time. // This is useful for functions which have side-effects or are not deterministically computable. +// +// The implementation is supplied at evaluation time as a functions.LateBoundOp within the function +// namespace of the activation, and is subject to the same runtime type-guard as an eager binding. func LateFunctionBinding() OverloadOpt { return func(o *OverloadDecl) (*OverloadDecl, error) { if o.HasBinding() { diff --git a/vendor/github.com/google/cel-go/common/doc.go b/vendor/cel.dev/cel-go/common/doc.go similarity index 100% rename from vendor/github.com/google/cel-go/common/doc.go rename to vendor/cel.dev/cel-go/common/doc.go diff --git a/vendor/github.com/google/cel-go/common/env/BUILD.bazel b/vendor/cel.dev/cel-go/common/env/BUILD.bazel similarity index 94% rename from vendor/github.com/google/cel-go/common/env/BUILD.bazel rename to vendor/cel.dev/cel-go/common/env/BUILD.bazel index 261da924d43..2dc1af513fc 100644 --- a/vendor/github.com/google/cel-go/common/env/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/env/BUILD.bazel @@ -22,10 +22,11 @@ package( go_library( name = "go_default_library", srcs = [ + "catalog.go", "env.go", "io.go", ], - importpath = "github.com/google/cel-go/common/env", + importpath = "cel.dev/cel-go/common/env", deps = [ "//common:go_default_library", "//common/decls:go_default_library", @@ -38,6 +39,7 @@ go_test( name = "go_default_test", size = "small", srcs = [ + "catalog_test.go", "env_test.go", "io_test.go", ], diff --git a/vendor/cel.dev/cel-go/common/env/catalog.go b/vendor/cel.dev/cel-go/common/env/catalog.go new file mode 100644 index 00000000000..aed5ac42f84 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/env/catalog.go @@ -0,0 +1,294 @@ +// 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 env + +import ( + "math" + "sort" + "strings" +) + +// SymbolKind indicates the kind of declaration provided in an environment. +type SymbolKind string + +const ( + // FunctionKind represents a function declaration. + FunctionKind SymbolKind = "function" + + // MacroKind represents a macro declaration. + MacroKind SymbolKind = "macro" + + // TypeKind represents a type declaration. + TypeKind SymbolKind = "type" + + // NamespaceKind represents a namespace declaration. + NamespaceKind SymbolKind = "namespace" + + // VariableKind represents a variable declaration. + VariableKind SymbolKind = "variable" +) + +// CatalogSymbol represents a symbol in the environment catalog. +type CatalogSymbol struct { + Name string `yaml:"name" json:"name"` + Kind SymbolKind `yaml:"kind" json:"kind"` + Library string `yaml:"library,omitempty" json:"library,omitempty"` + Option string `yaml:"option,omitempty" json:"option,omitempty"` + MinVersion uint32 `yaml:"min_version,omitempty" json:"minVersion,omitempty"` +} + +// Catalog holds an index of symbols from the active environment and available libraries. +type Catalog struct { + symbols []*CatalogSymbol + byName map[string]*CatalogSymbol +} + +// NewCatalog creates a new Catalog initialized with the given symbols. +func NewCatalog(symbols ...*CatalogSymbol) *Catalog { + c := &Catalog{ + symbols: make([]*CatalogSymbol, 0, len(symbols)), + byName: make(map[string]*CatalogSymbol, len(symbols)), + } + for _, sym := range symbols { + c.Add(sym) + } + return c +} + +// Add adds one or more symbols to the catalog. +func (c *Catalog) Add(syms ...*CatalogSymbol) { + for _, sym := range syms { + if sym == nil || sym.Name == "" { + continue + } + c.symbols = append(c.symbols, sym) + c.byName[sym.Name] = sym + } +} + +// Find searches for symbols in the catalog matching the given name. +// +// If an exact match exists, a single-element slice containing that symbol is returned. +// Otherwise, candidate symbols matching the namespace structure (namespaced vs non-namespaced) +// are evaluated using Damerau-Levenshtein edit distance within the following thresholds: +// - length <= 3: max distance 1 +// - 4 <= length <= 8: max distance 2 +// - length > 8: max distance length / 3 +// +// If candidates meet the edit distance threshold, up to two entries with the lowest edit distance +// are returned, ordered from shortest to longest suggestion (and alphabetically on tie). +// +// If no edit distance matches are found, prefix matching is performed as a fallback. When a prefix +// matches multiple symbols, up to two candidates are returned, ordered from shortest to longest. +func (c *Catalog) Find(name string) []*CatalogSymbol { + if c == nil || name == "" { + return nil + } + if sym, ok := c.byName[name]; ok { + return []*CatalogSymbol{sym} + } + if len(c.symbols) == 0 { + return nil + } + + hasDot := strings.Contains(name, ".") + maxDist := 2 + if len(name) <= 3 { + maxDist = 1 + } else if len(name) > 8 { + maxDist = len(name) / 3 + } + + type match struct { + sym *CatalogSymbol + dist int + } + var editMatches []match + minDist := math.MaxInt32 + seen := make(map[string]bool) + + for _, sym := range c.symbols { + if seen[sym.Name] { + continue + } + // If the input might be namespaced, only consider other namespaced symbols. + if strings.Contains(sym.Name, ".") != hasDot { + continue + } + seen[sym.Name] = true + dist := EditDistance(name, sym.Name) + if dist <= maxDist { + editMatches = append(editMatches, match{sym: sym, dist: dist}) + if dist < minDist { + minDist = dist + } + } + } + + if len(editMatches) > 0 { + var best []match + for _, m := range editMatches { + if m.dist == minDist { + best = append(best, m) + } + } + sort.Slice(best, func(i, j int) bool { + if len(best[i].sym.Name) != len(best[j].sym.Name) { + return len(best[i].sym.Name) < len(best[j].sym.Name) + } + return best[i].sym.Name < best[j].sym.Name + }) + resultCount := min(len(best), 2) + res := make([]*CatalogSymbol, resultCount) + for i := range res { + res[i] = best[i].sym + } + return res + } + + // Fallback: prefix match (require at least 2 characters) + if len(name) >= 2 { + seenPrefix := make(map[string]bool) + var prefixMatches []*CatalogSymbol + for _, sym := range c.symbols { + if seenPrefix[sym.Name] { + continue + } + if strings.Contains(sym.Name, ".") != hasDot { + continue + } + if strings.HasPrefix(sym.Name, name) { + seenPrefix[sym.Name] = true + prefixMatches = append(prefixMatches, sym) + } + } + + if len(prefixMatches) > 0 { + sort.Slice(prefixMatches, func(i, j int) bool { + if len(prefixMatches[i].Name) != len(prefixMatches[j].Name) { + return len(prefixMatches[i].Name) < len(prefixMatches[j].Name) + } + return prefixMatches[i].Name < prefixMatches[j].Name + }) + if len(prefixMatches) > 2 { + prefixMatches = prefixMatches[:2] + } + return prefixMatches + } + } + + return nil +} + +const ( + maxEditDistanceIterations = 64 * 64 +) + +// EditDistance calculates the Damerau-Levenshtein distance between two strings, +// taking into account insertions, deletions, substitutions, and adjacent transpositions. +// +// When evaluating string similarity for suggestions, the following distance thresholds are recommended: +// - length <= 3: max distance 1 +// - 4 <= length <= 8: max distance 2 +// - length > 8: max distance length / 3 +func EditDistance(a, b string) int { + ra, rb := []rune(a), []rune(b) + la, lb := len(ra), len(rb) + if la == 0 { + return lb + } + if lb == 0 { + return la + } + if la >= 16 && la*2 >= lb*3 { + return la - lb + } + if lb >= 16 && lb*2 >= la*3 { + return lb - la + } + if la*lb > maxEditDistanceIterations { + return math.MaxInt32 + } + + maxLen := max(la, lb) + threshold := maxLen / 3 + + d := make([][]int, la+1) + for i := range d { + d[i] = make([]int, lb+1) + d[i][0] = i + } + for j := 0; j <= lb; j++ { + d[0][j] = j + } + + for i := 1; i <= la; i++ { + rowMin := d[i][0] + for j := 1; j <= lb; j++ { + cost := 1 + if ra[i-1] == rb[j-1] { + cost = 0 + } + d[i][j] = min( + d[i-1][j]+1, // deletion + d[i][j-1]+1, // insertion + d[i-1][j-1]+cost, // substitution + ) + if i > 1 && j > 1 && ra[i-1] == rb[j-2] && ra[i-2] == rb[j-1] { + d[i][j] = min(d[i][j], d[i-2][j-2]+1) // transposition + } + if d[i][j] < rowMin { + rowMin = d[i][j] + } + } + if rowMin > threshold { + return math.MaxInt32 + } + } + return d[la][lb] +} + +// ToCatalog converts the Config to a Catalog containing its functions, variables, container, and imports. +func (c *Config) ToCatalog() *Catalog { + cat := NewCatalog() + if c == nil { + return cat + } + for _, v := range c.Variables { + cat.Add(&CatalogSymbol{ + Name: v.Name, + Kind: VariableKind, + }) + } + for _, f := range c.Functions { + cat.Add(&CatalogSymbol{ + Name: f.Name, + Kind: FunctionKind, + }) + } + if c.Container != "" { + cat.Add(&CatalogSymbol{ + Name: c.Container, + Kind: NamespaceKind, + }) + } + for _, imp := range c.Imports { + cat.Add(&CatalogSymbol{ + Name: imp.Name, + Kind: NamespaceKind, + }) + } + return cat +} diff --git a/vendor/github.com/google/cel-go/common/env/env.go b/vendor/cel.dev/cel-go/common/env/env.go similarity index 86% rename from vendor/github.com/google/cel-go/common/env/env.go rename to vendor/cel.dev/cel-go/common/env/env.go index 936036ed21c..433724ce48d 100644 --- a/vendor/github.com/google/cel-go/common/env/env.go +++ b/vendor/cel.dev/cel-go/common/env/env.go @@ -22,11 +22,11 @@ import ( "strconv" "strings" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/types" ) -// NewConfig creates an instance of a YAML serializable CEL environment configuration. +// NewConfig creates an instance of a YAML/JSON serializable CEL environment configuration. func NewConfig(name string) *Config { return &Config{ Name: name, @@ -39,18 +39,18 @@ func NewConfig(name string) *Config { // considered part of the core CEL environment configuration and should be managed separately // until a common convention for such settings is developed. type Config struct { - Name string `yaml:"name,omitempty"` - Description string `yaml:"description,omitempty"` - Container string `yaml:"container,omitempty"` - Imports []*Import `yaml:"imports,omitempty"` - StdLib *LibrarySubset `yaml:"stdlib,omitempty"` - Extensions []*Extension `yaml:"extensions,omitempty"` - ContextVariable *ContextVariable `yaml:"context_variable,omitempty"` - Variables []*Variable `yaml:"variables,omitempty"` - Functions []*Function `yaml:"functions,omitempty"` - Validators []*Validator `yaml:"validators,omitempty"` - Features []*Feature `yaml:"features,omitempty"` - Limits []*Limit `yaml:"limits,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Container string `yaml:"container,omitempty" json:"container,omitempty" jsonschema_description:"Protobuf container namespace for type resolution"` + Imports []*Import `yaml:"imports,omitempty" json:"imports,omitempty" jsonschema_description:"Fully qualified type names imported by simple name"` + StdLib *LibrarySubset `yaml:"stdlib,omitempty" json:"stdlib,omitempty" jsonschema_description:"Standard library macro and function subset configuration"` + Extensions []*Extension `yaml:"extensions,omitempty" json:"extensions,omitempty"` + ContextVariable *ContextVariable `yaml:"context_variable,omitempty" json:"contextVariable,omitempty" jsonschema_description:"Protobuf message type whose fields are exposed as top-level variables"` + Variables []*Variable `yaml:"variables,omitempty" json:"variables,omitempty"` + Functions []*Function `yaml:"functions,omitempty" json:"functions,omitempty"` + Validators []*Validator `yaml:"validators,omitempty" json:"validators,omitempty"` + Features []*Feature `yaml:"features,omitempty" json:"features,omitempty"` + Limits []*Limit `yaml:"limits,omitempty" json:"limits,omitempty"` } // Validate validates the whole configuration is well-formed. @@ -226,7 +226,7 @@ func NewImport(name string) *Import { // Import represents a type name that will be appreviated by its simple name using // the cel.Abbrevs() option. type Import struct { - Name string `yaml:"name"` + Name string `yaml:"name" json:"name" jsonschema_description:"Fully qualified protobuf message or enum name"` } // Validate validates the import configuration is well-formed. @@ -253,15 +253,15 @@ func NewVariableWithDoc(name string, t *TypeDesc, doc string) *Variable { // Variable represents a typed variable declaration which will be published via the // cel.VariableDecls() option. type Variable struct { - Name string `yaml:"name"` - Description string `yaml:"description,omitempty"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` // Type represents the type declaration for the variable. // // When serialized, 'type' is used for shorthand specifier string. // // Use GetType() for getting the effective type. - Type *TypeDesc `yaml:"type,omitempty"` + Type *TypeDesc `yaml:"type,omitempty" json:"type,omitempty" jsonschema_description:"Type specifier formatted as TypeName or namespace.TypeName with optional type parameters in angle brackets <>"` // TypeDesc is an embedded set of fields allowing for the specification of the Variable type. *TypeDesc `yaml:",inline"` @@ -323,7 +323,7 @@ func NewContextVariable(typeName string) *ContextVariable { type ContextVariable struct { // TypeName represents the fully qualified typename of the context variable. // Currently, only protobuf types are supported. - TypeName string `yaml:"type_name"` + TypeName string `yaml:"type_name" json:"typeName" jsonschema_description:"Fully qualified protobuf message type name"` } // Validate validates the context-variable configuration is well-formed. @@ -349,9 +349,9 @@ func NewFunctionWithDoc(name, doc string, overloads ...*Overload) *Function { // Function represents the serializable format of a function and its overloads. type Function struct { - Name string `yaml:"name"` - Description string `yaml:"description,omitempty"` - Overloads []*Overload `yaml:"overloads,omitempty"` + Name string `yaml:"name" json:"name" jsonschema_description:"camelCase function name, either standalone or namespace.functionName"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Overloads []*Overload `yaml:"overloads,omitempty" json:"overloads,omitempty"` } // Validate validates the function configuration is well-formed. @@ -405,11 +405,11 @@ func NewMemberOverload(id string, target *TypeDesc, args []*TypeDesc, ret *TypeD // Overload represents the serializable format of a function overload. type Overload struct { - ID string `yaml:"id"` - Examples []string `yaml:"examples,omitempty"` - Target *TypeDesc `yaml:"target,omitempty"` - Args []*TypeDesc `yaml:"args,omitempty"` - Return *TypeDesc `yaml:"return,omitempty"` + ID string `yaml:"id" json:"id" jsonschema_description:"Overload ID formatted as function_name_type1_..._typeN or target_type_function_name_type1_..._typeN"` + Examples []string `yaml:"examples,omitempty" json:"examples,omitempty"` + Target *TypeDesc `yaml:"target,omitempty" json:"target,omitempty" jsonschema_description:"Receiver type specifier for member functions"` + Args []*TypeDesc `yaml:"args,omitempty" json:"args,omitempty"` + Return *TypeDesc `yaml:"return,omitempty" json:"return,omitempty"` } // Validate validates the overload configuration is well-formed. @@ -458,14 +458,16 @@ func (od *Overload) AsFunctionOption(tp types.Provider) (decls.FunctionOpt, erro if od.Target != nil { t, err := od.Target.AsCELType(tp) if err != nil { - return nil, errors.Join(append(errs, err)...) + errs = append(errs, err) } args = append([]*types.Type{t}, args...) - return decls.MemberOverload(od.ID, args, result), nil } if len(errs) != 0 { return nil, errors.Join(errs...) } + if od.Target != nil { + return decls.MemberOverload(od.ID, args, result, decls.OverloadExamples(od.Examples...)), nil + } return decls.Overload(od.ID, args, result, decls.OverloadExamples(od.Examples...)), nil } @@ -484,10 +486,10 @@ func NewExtension(name string, version uint32) *Extension { // Extension represents a named and optionally versioned extension library configured in the environment. type Extension struct { // Name is either the LibraryName() or some short-hand simple identifier which is understood by the config-handler. - Name string `yaml:"name"` + Name string `yaml:"name" json:"name"` // Version may either be an unsigned long value or the string 'latest'. If empty, the value is treated as '0'. - Version string `yaml:"version,omitempty"` + Version string `yaml:"version,omitempty" json:"version,omitempty" jsonschema_description:"Extension version number or 'latest'"` } // Validate validates the extension configuration is well-formed. @@ -526,28 +528,28 @@ func NewLibrarySubset() *LibrarySubset { type LibrarySubset struct { // Disabled indicates whether the library has been disabled, typically only used for // default-enabled libraries like stdlib. - Disabled bool `yaml:"disabled,omitempty"` + Disabled bool `yaml:"disabled,omitempty" json:"disabled,omitempty"` // DisableMacros disables macros for the given library. - DisableMacros bool `yaml:"disable_macros,omitempty"` + DisableMacros bool `yaml:"disable_macros,omitempty" json:"disableMacros,omitempty"` // IncludeMacros specifies a set of macro function names to include in the subset. - IncludeMacros []string `yaml:"include_macros,omitempty"` + IncludeMacros []string `yaml:"include_macros,omitempty" json:"includeMacros,omitempty"` // ExcludeMacros specifies a set of macro function names to exclude from the subset. // Note: if IncludeMacros is non-empty, then ExcludeFunctions is ignored. - ExcludeMacros []string `yaml:"exclude_macros,omitempty"` + ExcludeMacros []string `yaml:"exclude_macros,omitempty" json:"excludeMacros,omitempty"` // IncludeFunctions specifies a set of functions to include in the subset. // // Note: the overloads specified in the subset need only specify their ID. // Note: if IncludeFunctions is non-empty, then ExcludeFunctions is ignored. - IncludeFunctions []*Function `yaml:"include_functions,omitempty"` + IncludeFunctions []*Function `yaml:"include_functions,omitempty" json:"includeFunctions,omitempty" jsonschema_description:"Functions and optional overload IDs to include"` // ExcludeFunctions specifies the set of functions to exclude from the subset. // // Note: the overloads specified in the subset need only specify their ID. - ExcludeFunctions []*Function `yaml:"exclude_functions,omitempty"` + ExcludeFunctions []*Function `yaml:"exclude_functions,omitempty" json:"excludeFunctions,omitempty" jsonschema_description:"Functions and optional overload IDs to exclude"` } // Validate validates the library configuration is well-formed. @@ -699,8 +701,8 @@ func NewValidator(name string) *Validator { // Note: the map-keys must directly correspond to the internal representation of the original // validator, and should only use primitive scalar types as values at this time. type Validator struct { - Name string `yaml:"name"` - Config map[string]any `yaml:"config,omitempty"` + Name string `yaml:"name" json:"name"` + Config map[string]any `yaml:"config,omitempty" json:"config,omitempty"` } // Validate validates the configuration of the validator object. @@ -736,8 +738,8 @@ func NewFeature(name string, enabled bool) *Feature { // Feature represents a named boolean feature flag supported by CEL. type Feature struct { - Name string `yaml:"name"` - Enabled bool `yaml:"enabled"` + Name string `yaml:"name" json:"name"` + Enabled bool `yaml:"enabled" json:"enabled"` } // Validate validates whether the feature is well-configured. @@ -754,8 +756,8 @@ func (feat *Feature) Validate() error { // Limit represents a named limit in the CEL environment. This is used to control // the complexity tolerated before failing parsing, type checking, or planning. type Limit struct { - Name string `yaml:"name"` - Value int `yaml:"value"` + Name string `yaml:"name" json:"name"` + Value int `yaml:"value" json:"value"` } // NewLimit creates a new limit. @@ -786,9 +788,9 @@ func NewTypeParam(paramName string) *TypeDesc { // TypeDesc represents the serializable format of a CEL *types.Type value. type TypeDesc struct { - TypeName string `yaml:"type_name"` - Params []*TypeDesc `yaml:"params,omitempty"` - IsTypeParam bool `yaml:"is_type_param,omitempty"` + TypeName string `yaml:"type_name" json:"typeName"` + Params []*TypeDesc `yaml:"params,omitempty" json:"params,omitempty"` + IsTypeParam bool `yaml:"is_type_param,omitempty" json:"isTypeParam,omitempty"` } // String implements the strings.Stringer interface method. @@ -950,9 +952,8 @@ func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) { if !found { return nil, fmt.Errorf("undefined type name: %q", td.TypeName) } - _, ok := t.(*types.Type) - if ok && len(td.Params) == 0 { - return t.(*types.Type), nil + if celType, ok := t.(*types.Type); ok && len(td.Params) == 0 { + return celType, nil } params := make([]*types.Type, len(td.Params)) for i, p := range td.Params { diff --git a/vendor/cel.dev/cel-go/common/env/io.go b/vendor/cel.dev/cel-go/common/env/io.go new file mode 100644 index 00000000000..621ee305fd5 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/env/io.go @@ -0,0 +1,528 @@ +// 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 env + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "strings" + + "go.yaml.in/yaml/v3" +) + +// YAML and JSON Serialization Methodology: +// +// Several Config sub-types support both structured object representations and shorthand/alias +// forms when decoding (e.g., TypeDesc specifier strings like "map", scalar Overload +// IDs, "type" aliases on ContextVariable, and inline vs nested TypeDesc fields on Variable), while +// normalizing to canonical shorthand representations when encoding. +// +// To keep YAML and JSON handling symmetric while satisfying each library's canonical interfaces: +// +// 1. Unmarshaling: Each customized type implements an internal `unmarshal(func(any) error) error` +// method that decodes into a parallel internal struct (avoiding recursive Unmarshaler loops). +// - `UnmarshalYAML(unmarshal func(any) error) error` passes the active `yaml.v3` decoder +// closure directly to `unmarshal(...)`. Using the `func(any) error` signature (rather than +// `UnmarshalYAML(*yaml.Node)`) preserves the outer `yaml.Decoder`'s `KnownFields` setting +// when `StrictDecoding(true)` is enabled, whereas `(*yaml.Node).Decode` constructs a fresh +// decoder with `KnownFields` disabled. +// - `UnmarshalJSON(data []byte) error` satisfies `encoding/json.Unmarshaler` and adapts +// `json.Unmarshal` to the same `func(any) error` signature via `jsonUnmarshaler(data)`. +// +// 2. Marshaling: Types that require custom output formatting (`Variable` and `TypeDesc`) implement +// an internal `marshal() any` method returning the canonical value to serialize: +// - `MarshalYAML() (any, error)` returns `marshal(), nil` for `yaml.v3` to encode. +// - `MarshalJSON() ([]byte, error)` encodes `marshal()` via `marshalJSONNoEscape` so type +// specifier brackets (`<`, `>`) are not HTML-escaped. +// - `Variable` requires an explicit marshaler because it embeds `*TypeDesc` (`yaml:",inline"`), +// and `yaml.v3` omits inlined structs that implement `Unmarshaler` during `yaml.Marshal` +// (while `encoding/json` would otherwise promote `TypeDesc.MarshalJSON` to `Variable`). +// Delegating `Variable.marshal()` to `&internalVariable{..., Type: v.GetType()}` allows both +// `yaml.v3` and `encoding/json` to invoke `TypeDesc`'s respective marshaler on `Type`. + +type internalContextVariable struct { + Type string `yaml:"type,omitempty" json:"type,omitempty"` + TypeName string `yaml:"type_name,omitempty" json:"typeName,omitempty"` +} + +type internalOverload Overload + +type internalTypeDesc TypeDesc + +// Embedding TypeDesc in variable causes issues with customizing +// unmarshalling / marshalling. Work around with a parallel type. +type internalVariable struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Type *TypeDesc `yaml:"type,omitempty" json:"type,omitempty"` + TypeName string `yaml:"type_name,omitempty" json:"typeName,omitempty"` + Params []*TypeDesc `yaml:"params,omitempty" json:"params,omitempty"` + IsTypeParam bool `yaml:"is_type_param,omitempty" json:"isTypeParam,omitempty"` +} + +type internalValidator Validator + +func jsonUnmarshaler(data []byte) func(any) error { + return func(v any) error { + return json.Unmarshal(data, v) + } +} + +func (ctx *ContextVariable) unmarshal(unmarshal func(any) error) error { + var buf internalContextVariable + if err := unmarshal(&buf); err != nil { + return err + } + if buf.TypeName != "" { + ctx.TypeName = buf.TypeName + } else if buf.Type != "" { + ctx.TypeName = buf.Type + } + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (ctx *ContextVariable) UnmarshalYAML(unmarshal func(any) error) error { + return ctx.unmarshal(unmarshal) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (ctx *ContextVariable) UnmarshalJSON(data []byte) error { + return ctx.unmarshal(jsonUnmarshaler(data)) +} + +func (od *Overload) unmarshal(unmarshal func(any) error) error { + var id string + if err := unmarshal(&id); err == nil { + od.ID = id + return nil + } + var buf internalOverload + if err := unmarshal(&buf); err != nil { + return fmt.Errorf("unsupported overload encoding: %w", err) + } + *od = Overload(buf) + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (od *Overload) UnmarshalYAML(unmarshal func(any) error) error { + return od.unmarshal(unmarshal) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (od *Overload) UnmarshalJSON(data []byte) error { + return od.unmarshal(jsonUnmarshaler(data)) +} + +func (v *Variable) unmarshal(unmarshal func(any) error) error { + var buf internalVariable + if err := unmarshal(&buf); err != nil { + return err + } + v.Name = buf.Name + v.Description = buf.Description + if buf.TypeName != "" { + v.TypeDesc = &TypeDesc{ + TypeName: buf.TypeName, + Params: buf.Params, + IsTypeParam: buf.IsTypeParam, + } + } else if buf.Type != nil { + v.TypeDesc = buf.Type + } + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (v *Variable) UnmarshalYAML(unmarshal func(any) error) error { + return v.unmarshal(unmarshal) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *Variable) UnmarshalJSON(data []byte) error { + return v.unmarshal(jsonUnmarshaler(data)) +} + +func (v *Variable) marshal() any { + return &internalVariable{ + Name: v.Name, + Description: v.Description, + Type: v.GetType(), + } +} + +// MarshalYAML implements yaml.Marshaler. +func (v *Variable) MarshalYAML() (any, error) { + return v.marshal(), nil +} + +// MarshalJSON implements json.Marshaler. +func (v *Variable) MarshalJSON() ([]byte, error) { + return marshalJSONNoEscape(v.marshal(), "") +} + +func (td *TypeDesc) unmarshal(unmarshal func(any) error) error { + var str string + if err := unmarshal(&str); err == nil { + o, err := ParseTypeDesc(str) + if err != nil { + return err + } + *td = *o + return nil + } + var buf internalTypeDesc + if err := unmarshal(&buf); err != nil { + return fmt.Errorf("unsupported type description encoding: %w", err) + } + *td = TypeDesc(buf) + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (td *TypeDesc) UnmarshalYAML(unmarshal func(any) error) error { + return td.unmarshal(unmarshal) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (td *TypeDesc) UnmarshalJSON(data []byte) error { + return td.unmarshal(jsonUnmarshaler(data)) +} + +func (td *TypeDesc) marshal() any { + if td == nil { + return nil + } + return td.SpecifierFormat() +} + +// MarshalYAML implements yaml.Marshaler. +func (td *TypeDesc) MarshalYAML() (any, error) { + return td.marshal(), nil +} + +// MarshalJSON implements json.Marshaler. +func (td *TypeDesc) MarshalJSON() ([]byte, error) { + return marshalJSONNoEscape(td.marshal(), "") +} + +func (v *Validator) unmarshal(unmarshal func(any) error) error { + var buf internalValidator + if err := unmarshal(&buf); err != nil { + return err + } + for k, val := range buf.Config { + if num, ok := val.(float64); ok && num == float64(int(num)) { + buf.Config[k] = int(num) + } + } + *v = Validator(buf) + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (v *Validator) UnmarshalYAML(unmarshal func(any) error) error { + return v.unmarshal(unmarshal) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *Validator) UnmarshalJSON(data []byte) error { + return v.unmarshal(jsonUnmarshaler(data)) +} + +type typeDescParser struct { + text string + pos int + length int +} + +// ParseTypeDesc parses a TypeDesc from the type specifier format: "map" +func ParseTypeDesc(text string) (*TypeDesc, error) { + p := &typeDescParser{text: text, length: len(text)} + res, err := p.parseTypeElem() + if err != nil { + return nil, fmt.Errorf("failed to parse type %q: %v", text, err) + } + p.skipWhitespace() + if p.pos < p.length { + return nil, fmt.Errorf("unexpected character %q at position %d in %q", p.text[p.pos], p.pos, text) + } + return res, nil +} + +func (p *typeDescParser) parseConcreteType() (*TypeDesc, error) { + id, err := p.parseNamespaceIdentifier() + if err != nil { + return nil, err + } + if p.pos < p.length && p.text[p.pos] == '<' { + p.pos++ // consume '<' + var params []*TypeDesc + for { + p.skipWhitespace() + param, err := p.parseTypeElem() + if err != nil { + return nil, err + } + params = append(params, param) + p.skipWhitespace() + if p.pos < p.length && p.text[p.pos] == ',' { + p.pos++ // consume ',' + continue + } + if p.pos < p.length && p.text[p.pos] == '>' { + p.pos++ // consume '>' + break + } + return nil, fmt.Errorf("expected ',' or '>' at position %d", p.pos) + } + return NewTypeDesc(id, params...), nil + } + return NewTypeDesc(id), nil +} + +func (p *typeDescParser) parseTypeElem() (*TypeDesc, error) { + p.skipWhitespace() + if p.pos < p.length && p.text[p.pos] == '~' { + p.pos++ // consume '~' + id, err := p.parseTypeParamIdent() + if err != nil { + return nil, err + } + return NewTypeParam(id), nil + } + return p.parseConcreteType() +} + +func (p *typeDescParser) parseNamespaceIdentifier() (string, error) { + p.skipWhitespace() + var id string + for p.pos < p.length && p.text[p.pos] != '<' { + c := p.text[p.pos] + if c == '.' { + id += "." + p.pos++ // consume '.' + } + ident, err := p.parseIdentifier() + if err != nil { + return "", err + } + id += ident + p.skipWhitespace() + if p.pos < p.length && p.text[p.pos] != '.' { + break + } + } + if id == "" { + return "", fmt.Errorf("missing identifier at position %d", p.pos) + } + return id, nil +} + +func (p *typeDescParser) parseIdentifier() (string, error) { + p.skipWhitespace() + if p.pos >= p.length { + return "", fmt.Errorf("unexpected end of input") + } + start := p.pos + c := p.text[p.pos] + if !isAlpha(c) && c != '_' { + return "", fmt.Errorf("identifier is expected, but %q was found at position %d", c, p.pos) + } + p.pos++ + for p.pos < p.length { + c := p.text[p.pos] + if !isAlphaNumeric(c) && c != '_' { + break + } + p.pos++ + } + return p.text[start:p.pos], nil +} + +func (p *typeDescParser) parseTypeParamIdent() (string, error) { + p.skipWhitespace() + if p.pos >= p.length { + return "", fmt.Errorf("unexpected end of input") + } + c := p.text[p.pos] + if !isAlpha(c) { + return "", fmt.Errorf("invalid type parameter identifier %q at position %d, must be a single character from A-Z", c, p.pos) + } + p.pos++ + if p.pos < p.length && isAlpha(p.text[p.pos]) { + return "", fmt.Errorf("invalid type param, must have a single alphabetic character at position %d", p.pos) + } + return string(c), nil +} + +func (p *typeDescParser) skipWhitespace() { + for p.pos < p.length && isWhitespace(p.text[p.pos]) { + p.pos++ + } +} + +func isWhitespace(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' +} + +func isAlpha(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +func isAlphaNumeric(c byte) bool { + return isAlpha(c) || (c >= '0' && c <= '9') +} + +// DecodeOption configures how an environment Config is decoded from YAML or JSON. +type DecodeOption func(*decodeOptions) + +type decodeOptions struct { + strict bool +} + +// StrictDecoding configures whether unknown fields in the YAML or JSON input cause a decoding error. +// By default, unknown fields are ignored. +func StrictDecoding(strict bool) DecodeOption { + return func(opts *decodeOptions) { + opts.strict = strict + } +} + +func validateStrictJSON(v any, t reflect.Type) error { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + switch val := v.(type) { + case []any: + if t.Kind() == reflect.Slice { + for _, elem := range val { + if err := validateStrictJSON(elem, t.Elem()); err != nil { + return err + } + } + } + case map[string]any: + switch t { + case reflect.TypeFor[ContextVariable](): + t = reflect.TypeFor[internalContextVariable]() + case reflect.TypeFor[Variable](): + t = reflect.TypeFor[internalVariable]() + } + if t.Kind() != reflect.Struct { + return nil + } + for k, child := range val { + ft, ok := jsonFieldType(t, k) + if !ok { + return fmt.Errorf("json: unknown field %q", k) + } + if err := validateStrictJSON(child, ft); err != nil { + return err + } + } + } + return nil +} + +func jsonFieldType(t reflect.Type, name string) (reflect.Type, bool) { + for i := range t.NumField() { + f := t.Field(i) + tag, _, _ := strings.Cut(f.Tag.Get("json"), ",") + if tag == name { + return f.Type, true + } + } + return nil, false +} + +// ConfigFromYAML returns a config from YAML source. +// +// Adds custom parsing logic for normalizing shorthand for specifiying some fields +// in a YAML document (mainly the type-specifier shorthand). +// +// Using yaml.Unmarshal with any implementation should be sufficient for most +// cases. +func ConfigFromYAML(data []byte, opts ...DecodeOption) (*Config, error) { + dOpts := &decodeOptions{} + for _, opt := range opts { + opt(dOpts) + } + c := &Config{} + dec := yaml.NewDecoder(bytes.NewReader(data)) + dec.KnownFields(dOpts.strict) + if err := dec.Decode(c); err != nil && !errors.Is(err, io.EOF) { + return nil, err + } + return c, nil +} + +// ConfigToYAML returns the config serialized to YAML +// +// Provided as a convenience wrapper around a tested YAML Marshaler. +func ConfigToYAML(c *Config) ([]byte, error) { + return yaml.Marshal(c) +} + +// ConfigFromJSON returns a config from JSON source. +// +// Adds custom parsing logic for normalizing shorthand for specifying some fields +// in a JSON document (mainly the type-specifier shorthand). +// +// Using json.Unmarshal with any implementation should be sufficient for most +// cases. +func ConfigFromJSON(data []byte, opts ...DecodeOption) (*Config, error) { + dOpts := &decodeOptions{} + for _, opt := range opts { + opt(dOpts) + } + c := &Config{} + if err := json.Unmarshal(data, c); err != nil { + return nil, err + } + if dOpts.strict { + var raw any + _ = json.Unmarshal(data, &raw) + if err := validateStrictJSON(raw, reflect.TypeFor[Config]()); err != nil { + return nil, err + } + } + return c, nil +} + +// ConfigToJSON returns the config serialized to JSON +// +// Provided as a convenience wrapper around a tested JSON Marshaler. +func ConfigToJSON(c *Config) ([]byte, error) { + return marshalJSONNoEscape(c, " ") +} + +func marshalJSONNoEscape(v any, indent string) ([]byte, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if indent != "" { + enc.SetIndent("", indent) + } + if err := enc.Encode(v); err != nil { + return nil, err + } + return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil +} diff --git a/vendor/github.com/google/cel-go/common/error.go b/vendor/cel.dev/cel-go/common/error.go similarity index 100% rename from vendor/github.com/google/cel-go/common/error.go rename to vendor/cel.dev/cel-go/common/error.go diff --git a/vendor/github.com/google/cel-go/common/errors.go b/vendor/cel.dev/cel-go/common/errors.go similarity index 100% rename from vendor/github.com/google/cel-go/common/errors.go rename to vendor/cel.dev/cel-go/common/errors.go diff --git a/vendor/github.com/google/cel-go/common/functions/BUILD.bazel b/vendor/cel.dev/cel-go/common/functions/BUILD.bazel similarity index 83% rename from vendor/github.com/google/cel-go/common/functions/BUILD.bazel rename to vendor/cel.dev/cel-go/common/functions/BUILD.bazel index 3cc27d60ce3..4511d06f7e4 100644 --- a/vendor/github.com/google/cel-go/common/functions/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/functions/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "functions.go", ], - importpath = "github.com/google/cel-go/common/functions", + importpath = "cel.dev/cel-go/common/functions", deps = [ "//common/types/ref:go_default_library", ], diff --git a/vendor/github.com/google/cel-go/common/functions/functions.go b/vendor/cel.dev/cel-go/common/functions/functions.go similarity index 64% rename from vendor/github.com/google/cel-go/common/functions/functions.go rename to vendor/cel.dev/cel-go/common/functions/functions.go index 0c00781d9ce..bc48e33ee91 100644 --- a/vendor/github.com/google/cel-go/common/functions/functions.go +++ b/vendor/cel.dev/cel-go/common/functions/functions.go @@ -18,7 +18,7 @@ package functions import ( "context" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // Overload defines a named overload of a function, indicating an operand trait @@ -51,11 +51,28 @@ type Overload struct { // Async defines the overload with an AsyncOp implementation. May be nil. Async AsyncOp + // LateBound specifies whether the Overload has a late-binding that will be resolved + // at runtime from the activation. + LateBound bool + + // LateBoundDispatch validates the runtime arguments of a late-bound call against the + // declared overload signatures and reports the id of the matching overload. May be nil, + // in which case the arguments are not validated against the declaration. + LateBoundDispatch LateBoundDispatcher + // NonStrict specifies whether the Overload will tolerate arguments that // are types.Err or types.Unknown. NonStrict bool } +// LateBoundDispatcher reports the id of the declared overload whose signature matches the +// receiver style and runtime arguments of a call, or false if no declared overload matches. +// +// Late-bound implementations are supplied by the activation rather than the declaration, so this +// is the only opportunity to enforce agreement between a call and the signature it was checked +// against. +type LateBoundDispatcher func(memberStyle bool, args ...ref.Val) (string, bool) + // UnaryOp is a function that takes a single value and produces an output. type UnaryOp func(ref.Val) ref.Val @@ -66,6 +83,19 @@ type BinaryOp func(ref.Val, ref.Val) ref.Val // a value or error as a result. type FunctionOp func(...ref.Val) ref.Val +// LateBoundOp is the implementation of a function whose binding is supplied at evaluation time +// rather than at declaration time. +// +// Late-bound implementations are resolved from the activation by function name, in a namespace +// which is separate from variables, so a function with multiple overloads is served by a single +// implementation. The id of the overload which matched the call is provided so that such an +// implementation may distinguish between them. +// +// The activation is deliberately not an input to the call. Bindings are supplied for a single +// evaluation alongside the variables of that evaluation, so an implementation which depends on +// evaluation context should capture it in a closure when the activation is constructed. +type LateBoundOp func(overloadID string, args ...ref.Val) ref.Val + // AsyncOp is a function that accepts zero or more arguments and produces // a value or error asynchronously via a channel. // diff --git a/vendor/github.com/google/cel-go/common/location.go b/vendor/cel.dev/cel-go/common/location.go similarity index 100% rename from vendor/github.com/google/cel-go/common/location.go rename to vendor/cel.dev/cel-go/common/location.go diff --git a/vendor/github.com/google/cel-go/common/operators/BUILD.bazel b/vendor/cel.dev/cel-go/common/operators/BUILD.bazel similarity index 80% rename from vendor/github.com/google/cel-go/common/operators/BUILD.bazel rename to vendor/cel.dev/cel-go/common/operators/BUILD.bazel index b5b67f06238..21776cecc35 100644 --- a/vendor/github.com/google/cel-go/common/operators/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/operators/BUILD.bazel @@ -10,5 +10,5 @@ go_library( srcs = [ "operators.go", ], - importpath = "github.com/google/cel-go/common/operators", + importpath = "cel.dev/cel-go/common/operators", ) diff --git a/vendor/github.com/google/cel-go/common/operators/operators.go b/vendor/cel.dev/cel-go/common/operators/operators.go similarity index 100% rename from vendor/github.com/google/cel-go/common/operators/operators.go rename to vendor/cel.dev/cel-go/common/operators/operators.go diff --git a/vendor/github.com/google/cel-go/common/overloads/BUILD.bazel b/vendor/cel.dev/cel-go/common/overloads/BUILD.bazel similarity index 80% rename from vendor/github.com/google/cel-go/common/overloads/BUILD.bazel rename to vendor/cel.dev/cel-go/common/overloads/BUILD.bazel index e46e2f4830c..1f1eaa094ae 100644 --- a/vendor/github.com/google/cel-go/common/overloads/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/overloads/BUILD.bazel @@ -10,5 +10,5 @@ go_library( srcs = [ "overloads.go", ], - importpath = "github.com/google/cel-go/common/overloads", + importpath = "cel.dev/cel-go/common/overloads", ) diff --git a/vendor/github.com/google/cel-go/common/overloads/overloads.go b/vendor/cel.dev/cel-go/common/overloads/overloads.go similarity index 91% rename from vendor/github.com/google/cel-go/common/overloads/overloads.go rename to vendor/cel.dev/cel-go/common/overloads/overloads.go index 0e3a7244956..78c9c8ea016 100644 --- a/vendor/github.com/google/cel-go/common/overloads/overloads.go +++ b/vendor/cel.dev/cel-go/common/overloads/overloads.go @@ -166,6 +166,31 @@ const ( ExtFormatString = "string_format" ) +// Optional function overloads which short-circuit, and so need to be referenced in cost +// computations. +const ( + OptionalOrOptional = "optional_or_optional" + OptionalOrValueValue = "optional_orValue_value" +) + +// Optional function overloads which create, wrap or unwrap a value, and so need to be referenced in +// cost computations to carry the wrapped value's size through the optional. +const ( + OptionalNone = "optional_none" + OptionalOf = "optional_of" + OptionalOfNonZeroValue = "optional_ofNonZeroValue" + OptionalValue = "optional_value" +) + +// Optional index overloads, which qualify their operand in the same way as the non-optional index +// operations and so extend a field path during cost estimation. +const ( + OptIndexList = "list_optindex_optional_int" + OptIndexOptionalList = "optional_list_optindex_optional_int" + OptIndexMap = "map_optindex_optional_value" + OptIndexOptionalMap = "optional_map_optindex_optional_value" +) + // Time-based functions. const ( TimeGetFullYear = "getFullYear" diff --git a/vendor/github.com/google/cel-go/common/runes/BUILD.bazel b/vendor/cel.dev/cel-go/common/runes/BUILD.bazel similarity index 87% rename from vendor/github.com/google/cel-go/common/runes/BUILD.bazel rename to vendor/cel.dev/cel-go/common/runes/BUILD.bazel index bb30242cfaf..0674eaed1bd 100644 --- a/vendor/github.com/google/cel-go/common/runes/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/runes/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "buffer.go", ], - importpath = "github.com/google/cel-go/common/runes", + importpath = "cel.dev/cel-go/common/runes", ) go_test( diff --git a/vendor/github.com/google/cel-go/common/runes/buffer.go b/vendor/cel.dev/cel-go/common/runes/buffer.go similarity index 100% rename from vendor/github.com/google/cel-go/common/runes/buffer.go rename to vendor/cel.dev/cel-go/common/runes/buffer.go diff --git a/vendor/github.com/google/cel-go/common/source.go b/vendor/cel.dev/cel-go/common/source.go similarity index 99% rename from vendor/github.com/google/cel-go/common/source.go rename to vendor/cel.dev/cel-go/common/source.go index 9187e9b5c38..73237edf986 100644 --- a/vendor/github.com/google/cel-go/common/source.go +++ b/vendor/cel.dev/cel-go/common/source.go @@ -15,7 +15,7 @@ package common import ( - "github.com/google/cel-go/common/runes" + "cel.dev/cel-go/common/runes" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) diff --git a/vendor/github.com/google/cel-go/common/stdlib/BUILD.bazel b/vendor/cel.dev/cel-go/common/stdlib/BUILD.bazel similarity index 91% rename from vendor/github.com/google/cel-go/common/stdlib/BUILD.bazel rename to vendor/cel.dev/cel-go/common/stdlib/BUILD.bazel index 124dbea8195..dd34ac1e50b 100644 --- a/vendor/github.com/google/cel-go/common/stdlib/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/stdlib/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "standard.go", ], - importpath = "github.com/google/cel-go/common/stdlib", + importpath = "cel.dev/cel-go/common/stdlib", deps = [ "//common:go_default_library", "//common/decls:go_default_library", diff --git a/vendor/github.com/google/cel-go/common/stdlib/standard.go b/vendor/cel.dev/cel-go/common/stdlib/standard.go similarity index 97% rename from vendor/github.com/google/cel-go/common/stdlib/standard.go rename to vendor/cel.dev/cel-go/common/stdlib/standard.go index bbbf2f7d9bd..85429376958 100644 --- a/vendor/github.com/google/cel-go/common/stdlib/standard.go +++ b/vendor/cel.dev/cel-go/common/stdlib/standard.go @@ -16,27 +16,31 @@ package stdlib import ( + "fmt" + "math" "strconv" "strings" + "sync" "time" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) var ( - stdFunctions []*decls.FunctionDecl - stdTypes []*decls.VariableDecl - utcTZ = types.String("UTC") + initStandardOnce sync.Once + stdFunctions []*decls.FunctionDecl + stdTypes []*decls.VariableDecl + utcTZ = types.String("UTC") ) -func init() { +func initStandard() { paramA := types.NewTypeParamType("A") paramB := types.NewTypeParamType("B") listOfA := types.NewListType(paramA) @@ -114,11 +118,13 @@ func init() { // Comprehension short-circuiting related function function(operators.NotStrictlyFalse, + decls.DisableTypeGuards(true), decls.Overload(overloads.NotStrictlyFalse, argTypes(types.BoolType), types.BoolType, decls.OverloadIsNonStrict(), decls.UnaryBinding(notStrictlyFalse))), // Deprecated: __not_strictly_false__ function(operators.OldNotStrictlyFalse, + decls.DisableTypeGuards(true), decls.DisableDeclaration(true), // safe deprecation decls.Overload(operators.OldNotStrictlyFalse, argTypes(types.BoolType), types.BoolType, decls.OverloadIsNonStrict(), @@ -310,6 +316,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') < duration('1s') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntNegOne { return types.True @@ -367,6 +376,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') <= duration('1s') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntNegOne || cmp == types.IntZero { return types.True @@ -424,6 +436,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') > duration('1us') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntOne { return types.True @@ -481,6 +496,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('60s') >= duration('1m') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntOne || cmp == types.IntZero { return types.True @@ -888,11 +906,13 @@ func init() { // Functions returns the set of standard library function declarations and definitions for CEL. func Functions() []*decls.FunctionDecl { + initStandardOnce.Do(initStandard) return stdFunctions } // Types returns the set of standard library types for CEL. func Types() []*decls.VariableDecl { + initStandardOnce.Do(initStandard) return stdTypes } @@ -926,6 +946,11 @@ func noBinaryOverrides(rhs, lhs ref.Val) ref.Val { return types.NoSuchOverloadErr() } +func isNaN(val ref.Val) bool { + d, ok := val.(types.Double) + return ok && math.IsNaN(float64(d)) +} + func noFunctionOverrides(args ...ref.Val) ref.Val { return types.NoSuchOverloadErr() } @@ -1035,7 +1060,7 @@ func inTimeZone(ts, tz ref.Val) (time.Time, error) { } // If the input is not the name of a timezone (for example, 'US/Central'), it should be a numerical offset from UTC - // in the format ^(+|-)(0[0-9]|1[0-4]):[0-5][0-9]$. The numerical input is parsed in terms of hours and minutes. + // in the format ^(+|-)([01]\d|2[0-3]):[0-5][0-9]$. The numerical input is parsed in terms of hours and minutes. hr, err := strconv.Atoi(string(val[0:ind])) if err != nil { return time.Time{}, err @@ -1044,6 +1069,12 @@ func inTimeZone(ts, tz ref.Val) (time.Time, error) { if err != nil { return time.Time{}, err } + if hr < -23 || hr > 23 { + return time.Time{}, fmt.Errorf("timezone offset hours out of range [-23, 23]: %s", val) + } + if min < 0 || min > 59 { + return time.Time{}, fmt.Errorf("timezone offset minutes out of range [0, 59]: %s", val) + } var offset int if string(val[0]) == "-" { offset = hr*60 - min diff --git a/vendor/github.com/google/cel-go/common/types/BUILD.bazel b/vendor/cel.dev/cel-go/common/types/BUILD.bazel similarity index 82% rename from vendor/github.com/google/cel-go/common/types/BUILD.bazel rename to vendor/cel.dev/cel-go/common/types/BUILD.bazel index 37d4df49543..da3231ba46d 100644 --- a/vendor/github.com/google/cel-go/common/types/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/types/BUILD.bazel @@ -8,6 +8,7 @@ package( go_library( name = "go_default_library", srcs = [ + "aggregate_sizer.go", "any_value.go", "bool.go", "bytes.go", @@ -21,19 +22,24 @@ go_library( "format.go", "list.go", "map.go", + "memory_tracker.go", + "native.go", "null.go", "object.go", "optional.go", "overflow.go", "provider.go", + "regex.go", + "size_calc.go", "string.go", + "struct.go", "timestamp.go", "types.go", "uint.go", "unknown.go", "util.go", ], - importpath = "github.com/google/cel-go/common/types", + importpath = "cel.dev/cel-go/common/types", deps = [ "//checker/decls:go_default_library", "//common/overloads:go_default_library", @@ -67,10 +73,14 @@ go_test( "json_struct_test.go", "list_test.go", "map_test.go", + "memory_tracker_test.go", + "native_test.go", "null_test.go", "object_test.go", "optional_test.go", "provider_test.go", + "regex_test.go", + "size_calc_test.go", "string_test.go", "timestamp_test.go", "types_test.go", @@ -80,13 +90,19 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//cel:go_default_library", + "//common/types/pb:go_default_library", "//common/types/ref:go_default_library", + "//common/types/traits:go_default_library", + "//ext:go_default_library", "//test:go_default_library", "//test/proto3pb:test_all_types_go_proto", "@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library", "@org_golang_google_protobuf//encoding/protojson:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_google_protobuf//types/known/anypb:go_default_library", "@org_golang_google_protobuf//types/known/durationpb:go_default_library", + "@org_golang_google_protobuf//types/known/structpb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", ], ) diff --git a/vendor/cel.dev/cel-go/common/types/aggregate_sizer.go b/vendor/cel.dev/cel-go/common/types/aggregate_sizer.go new file mode 100644 index 00000000000..d56b8bc4db6 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/aggregate_sizer.go @@ -0,0 +1,43 @@ +// 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 types + +// AggregateSizer calculates the recursive element size of values. +type AggregateSizer interface { + // AggregateSize returns the size of the input value, if known. + // Otherwise, a unit size of 1 is returned. + AggregateSize(val any) uint32 +} + +// AggregateSizeVisitor interface for ref.Val implementations capable of returning +// their total recursive element count. +type AggregateSizeVisitor interface { + // AggregateSize returns the total count of nested atomic elements (capped at math.MaxUint32). + AggregateSize(sizer AggregateSizer) uint32 +} + +// Helper for computing aggregate sizes of traits.Foldable types. +type foldableAggregateSizer struct { + sizer AggregateSizer + total uint32 +} + +// FoldEntry implements the traits.FoldEntry interface method and counts the aggregate size +// keys and values. +func (f *foldableAggregateSizer) FoldEntry(k, v any) bool { + f.total = safeAddUint32(f.total, f.sizer.AggregateSize(k)) + f.total = safeAddUint32(f.total, f.sizer.AggregateSize(v)) + return true +} diff --git a/vendor/github.com/google/cel-go/common/types/any_value.go b/vendor/cel.dev/cel-go/common/types/any_value.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/any_value.go rename to vendor/cel.dev/cel-go/common/types/any_value.go diff --git a/vendor/github.com/google/cel-go/common/types/bool.go b/vendor/cel.dev/cel-go/common/types/bool.go similarity index 98% rename from vendor/github.com/google/cel-go/common/types/bool.go rename to vendor/cel.dev/cel-go/common/types/bool.go index 5f1e4573e1a..d16964cdb18 100644 --- a/vendor/github.com/google/cel-go/common/types/bool.go +++ b/vendor/cel.dev/cel-go/common/types/bool.go @@ -20,7 +20,7 @@ import ( "strconv" "strings" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" diff --git a/vendor/github.com/google/cel-go/common/types/bytes.go b/vendor/cel.dev/cel-go/common/types/bytes.go similarity index 96% rename from vendor/github.com/google/cel-go/common/types/bytes.go rename to vendor/cel.dev/cel-go/common/types/bytes.go index 88da05315c4..206dd3bc5f9 100644 --- a/vendor/github.com/google/cel-go/common/types/bytes.go +++ b/vendor/cel.dev/cel-go/common/types/bytes.go @@ -22,7 +22,7 @@ import ( "strings" "unicode/utf8" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" @@ -44,7 +44,10 @@ func (b Bytes) Add(other ref.Val) ref.Val { if !ok { return ValOrErr(other, "no such overload") } - return append(b, otherBytes...) + sum := make([]byte, 0, len(b)+len(otherBytes)) + sum = append(sum, b...) + sum = append(sum, otherBytes...) + return Bytes(sum) } // Compare implements traits.Comparer interface method by lexicographic ordering. diff --git a/vendor/github.com/google/cel-go/common/types/compare.go b/vendor/cel.dev/cel-go/common/types/compare.go similarity index 97% rename from vendor/github.com/google/cel-go/common/types/compare.go rename to vendor/cel.dev/cel-go/common/types/compare.go index e1968261802..caad97b9413 100644 --- a/vendor/github.com/google/cel-go/common/types/compare.go +++ b/vendor/cel.dev/cel-go/common/types/compare.go @@ -17,7 +17,7 @@ package types import ( "math" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) func compareDoubleInt(d Double, i Int) Int { diff --git a/vendor/github.com/google/cel-go/common/types/doc.go b/vendor/cel.dev/cel-go/common/types/doc.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/doc.go rename to vendor/cel.dev/cel-go/common/types/doc.go diff --git a/vendor/github.com/google/cel-go/common/types/double.go b/vendor/cel.dev/cel-go/common/types/double.go similarity index 99% rename from vendor/github.com/google/cel-go/common/types/double.go rename to vendor/cel.dev/cel-go/common/types/double.go index 02abfee2dcf..729fc1ec41a 100644 --- a/vendor/github.com/google/cel-go/common/types/double.go +++ b/vendor/cel.dev/cel-go/common/types/double.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" diff --git a/vendor/github.com/google/cel-go/common/types/duration.go b/vendor/cel.dev/cel-go/common/types/duration.go similarity index 98% rename from vendor/github.com/google/cel-go/common/types/duration.go rename to vendor/cel.dev/cel-go/common/types/duration.go index 2207147734d..cd3efa1cec4 100644 --- a/vendor/github.com/google/cel-go/common/types/duration.go +++ b/vendor/cel.dev/cel-go/common/types/duration.go @@ -21,8 +21,8 @@ import ( "strings" "time" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" dpb "google.golang.org/protobuf/types/known/durationpb" diff --git a/vendor/github.com/google/cel-go/common/types/err.go b/vendor/cel.dev/cel-go/common/types/err.go similarity index 98% rename from vendor/github.com/google/cel-go/common/types/err.go rename to vendor/cel.dev/cel-go/common/types/err.go index 3216ff1c4af..354906a9fdc 100644 --- a/vendor/github.com/google/cel-go/common/types/err.go +++ b/vendor/cel.dev/cel-go/common/types/err.go @@ -19,7 +19,7 @@ import ( "fmt" "reflect" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // Error interface which allows types types.Err values to be treated as error values. @@ -36,7 +36,7 @@ type Err struct { var ( // ErrType singleton. - ErrType = NewOpaqueType("error") + ErrType = ErrorType // errDivideByZero is an error indicating a division by zero of an integer value. errDivideByZero = errors.New("division by zero") diff --git a/vendor/github.com/google/cel-go/common/types/format.go b/vendor/cel.dev/cel-go/common/types/format.go similarity index 91% rename from vendor/github.com/google/cel-go/common/types/format.go rename to vendor/cel.dev/cel-go/common/types/format.go index 174a2bd0405..3b2c06f0a9b 100644 --- a/vendor/github.com/google/cel-go/common/types/format.go +++ b/vendor/cel.dev/cel-go/common/types/format.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) type formattable interface { diff --git a/vendor/github.com/google/cel-go/common/types/int.go b/vendor/cel.dev/cel-go/common/types/int.go similarity index 99% rename from vendor/github.com/google/cel-go/common/types/int.go rename to vendor/cel.dev/cel-go/common/types/int.go index 60d5a716060..82dbb1738e5 100644 --- a/vendor/github.com/google/cel-go/common/types/int.go +++ b/vendor/cel.dev/cel-go/common/types/int.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" diff --git a/vendor/github.com/google/cel-go/common/types/iterator.go b/vendor/cel.dev/cel-go/common/types/iterator.go similarity index 94% rename from vendor/github.com/google/cel-go/common/types/iterator.go rename to vendor/cel.dev/cel-go/common/types/iterator.go index 98e9147b6ee..77fc90f4285 100644 --- a/vendor/github.com/google/cel-go/common/types/iterator.go +++ b/vendor/cel.dev/cel-go/common/types/iterator.go @@ -18,8 +18,8 @@ import ( "fmt" "reflect" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) var ( diff --git a/vendor/github.com/google/cel-go/common/types/json_value.go b/vendor/cel.dev/cel-go/common/types/json_value.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/json_value.go rename to vendor/cel.dev/cel-go/common/types/json_value.go diff --git a/vendor/cel.dev/cel-go/common/types/list.go b/vendor/cel.dev/cel-go/common/types/list.go new file mode 100644 index 00000000000..a9194f79d19 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/list.go @@ -0,0 +1,1344 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "sync" + "sync/atomic" + "unsafe" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + + anypb "google.golang.org/protobuf/types/known/anypb" + structpb "google.golang.org/protobuf/types/known/structpb" +) + +// NewDynamicList returns a traits.Lister with heterogenous elements. +// value should be an array of "native" types, i.e. any type that +// NativeToValue() can convert to a ref.Val. +func NewDynamicList(adapter Adapter, value any) traits.Lister { + if value == nil { + return NewList[any](adapter, nil) + } + switch v := value.(type) { + case []string: + return NewList(adapter, v) + case []ref.Val: + return NewList(adapter, v) + case []int: + return NewList(adapter, v) + case []int64: + return NewList(adapter, v) + case []int32: + return NewList(adapter, v) + case []uint: + return NewList(adapter, v) + case []uint64: + return NewList(adapter, v) + case []uint32: + return NewList(adapter, v) + case []float64: + return NewList(adapter, v) + case []float32: + return NewList(adapter, v) + case []bool: + return NewList(adapter, v) + case []any: + return NewList(adapter, v) + case [][]byte: + return NewList(adapter, v) + } + rt := reflect.TypeOf(value) + if meta := getDynamicSliceMeta(rt); meta.supported { + sh := (*unsafeSlice)(anyDataPtr(value)) + isNil := sh == nil || sh.Data == nil + if meta.isPtrElem { + var elems []unsafe.Pointer + if sh != nil && sh.Len > 0 { + elems = unsafe.Slice((*unsafe.Pointer)(sh.Data), sh.Len) + } + return &sliceList[unsafe.Pointer]{ + Adapter: adapter, + elems: elems, + elemTypePtr: meta.elemTypePtr, + meta: meta, + qualifyRawVal: meta.qualifyRawVal, + isNilSlice: isNil, + } + } + var elems []byte + if sh != nil && sh.Len > 0 { + elems = unsafe.Slice((*byte)(sh.Data), sh.Len*int(meta.elemStride)) + } + return &sliceList[byte]{ + Adapter: adapter, + elems: elems, + elemTypePtr: meta.elemTypePtr, + meta: meta, + qualifyRawVal: meta.qualifyRawVal, + isNilSlice: isNil, + } + } + refValue := reflect.ValueOf(value) + return &baseList{ + Adapter: adapter, + value: value, + size: refValue.Len(), + get: func(i int) any { + return refValue.Index(i).Interface() + }, + } +} + +// NewList returns a traits.Lister backed by a typed Go slice []T. +func NewList[T any](adapter Adapter, elems []T) traits.Lister { + return &sliceList[T]{ + Adapter: adapter, + elems: elems, + elemTypePtr: elemTypePtrFor[T](), + qualifyRawVal: isQualifyRawStruct[T](), + } +} + +// NewStringList returns a traits.Lister containing only strings. +func NewStringList(adapter Adapter, elems []string) traits.Lister { + return NewList(adapter, elems) +} + +// NewRefValList returns a traits.Lister with ref.Val elements. +// +// This type specialization is used with list literals within CEL expressions. +func NewRefValList(adapter Adapter, elems []ref.Val) traits.Lister { + return NewList(adapter, elems) +} + +// NewProtoList returns a traits.Lister based on a pb.List instance. +func NewProtoList(adapter Adapter, list protoreflect.List) traits.Lister { + return &baseList{ + Adapter: adapter, + value: list, + size: list.Len(), + get: func(i int) any { return list.Get(i).Interface() }, + } +} + +// NewJSONList returns a traits.Lister based on structpb.ListValue instance. +func NewJSONList(adapter Adapter, l *structpb.ListValue) traits.Lister { + vals := l.GetValues() + return &baseList{ + Adapter: adapter, + value: l, + size: len(vals), + get: func(i int) any { return vals[i] }, + } +} + +// NewMutableList creates a new mutable list whose internal state can be modified. +func NewMutableList(adapter Adapter) traits.MutableLister { + return &mutableList{ + sliceList: &sliceList[ref.Val]{ + Adapter: adapter, + }, + } +} + +// MaybeSliceList attempts to sub-slice a traits.Lister using its underlying native slice representation +// via reflection, preserving the original element type. If the underlying representation is not a slice +// or the indices are invalid, it returns (nil, false). +func MaybeSliceList(adapter Adapter, list traits.Lister, start, end int) (traits.Lister, bool) { + if list == nil { + return nil, false + } + if _, isConcat := list.(*concatList); isConcat { + return nil, false + } + val := list.Value() + if val == nil { + return nil, false + } + rv := reflect.ValueOf(val) + if rv.Kind() != reflect.Slice { + return nil, false + } + if start < 0 || end < 0 || start > end || end > rv.Len() { + return nil, false + } + sub := rv.Slice(start, end).Interface() + return NewDynamicList(adapter, sub), true +} + +// MaybeReverseList attempts to reverse a traits.Lister using its underlying native slice representation, +// preserving the original element type. If the underlying representation is not a slice, it returns (nil, false). +func MaybeReverseList(adapter Adapter, list traits.Lister) (traits.Lister, bool) { + if list == nil { + return nil, false + } + if _, isConcat := list.(*concatList); isConcat { + return nil, false + } + val := list.Value() + if val == nil { + return nil, false + } + rv := reflect.ValueOf(val) + if rv.Kind() != reflect.Slice { + return nil, false + } + n := rv.Len() + if n <= 1 { + return list, true + } + out := reflect.MakeSlice(rv.Type(), n, n) + for i := range n { + out.Index(n - 1 - i).Set(rv.Index(i)) + } + return NewDynamicList(adapter, out.Interface()), true +} + +// baseList points to a list containing elements of any type. +// The `value` is an array of native values, and refValue is its reflection object. +// The `Adapter` enables native type to CEL type conversions. +type baseList struct { + Adapter + value any + size int + // aggSize memoizes the aggregate size computed by the first completed sizing of this + // list. Accessed atomically since immutable lists may be shared across concurrent + // evaluations; zero means not yet computed. See the SizeCalculator documentation for + // the memoization contract. + aggSize uint32 + get func(int) any +} + +// Add implements the traits.Adder interface method. +func (l *baseList) Add(other ref.Val) ref.Val { + otherList, ok := other.(traits.Lister) + if !ok { + return MaybeNoSuchOverloadErr(other) + } + return newConcatList(l.Adapter, l, otherList) +} + +// Contains implements the traits.Container interface method. +func (l *baseList) Contains(elem ref.Val) ref.Val { + for i := 0; i < l.size; i++ { + val := l.NativeToValue(l.get(i)) + cmp := elem.Equal(val) + b, ok := cmp.(Bool) + if ok && b == True { + return True + } + } + return False +} + +// ConvertToNative implements the ref.Val interface method. +func (l *baseList) ConvertToNative(typeDesc reflect.Type) (any, error) { + return convertListToNative(l, l.value, typeDesc) +} + +func convertListToNative(l traits.Lister, val any, typeDesc reflect.Type) (any, error) { + if typeDesc == reflect.TypeFor[any]() { + typeDesc = reflect.TypeFor[[]any]() + } + // If the underlying list value is assignable to the reflected type return it. + if val != nil && reflect.TypeOf(val).AssignableTo(typeDesc) { + return val, nil + } + // If the list wrapper is assignable to the desired type return it. + if reflect.TypeOf(l).AssignableTo(typeDesc) { + return l, nil + } + // Attempt to convert the list to a set of well known protobuf types. + switch typeDesc { + case anyValueType: + json, err := l.ConvertToNative(JSONListType) + if err != nil { + return nil, err + } + return anypb.New(json.(proto.Message)) + case JSONValueType, JSONListType: + jsonValues, err := + l.ConvertToNative(reflect.TypeFor[[]*structpb.Value]()) + if err != nil { + return nil, err + } + jsonList := &structpb.ListValue{Values: jsonValues.([]*structpb.Value)} + if typeDesc == JSONListType { + return jsonList, nil + } + return structpb.NewListValue(jsonList), nil + } + // Non-list conversion. + if typeDesc.Kind() != reflect.Slice && typeDesc.Kind() != reflect.Array { + return nil, fmt.Errorf("type conversion error from list to '%v'", typeDesc) + } + + // List conversion. + // Allow the element ConvertToNative() function to determine whether conversion is possible. + otherElemType := typeDesc.Elem() + elemCount := int(l.Size().(Int)) + var nativeList reflect.Value + if typeDesc.Kind() == reflect.Array { + nativeList = reflect.New(reflect.ArrayOf(elemCount, typeDesc)).Elem().Index(0) + } else { + nativeList = reflect.MakeSlice(typeDesc, elemCount, elemCount) + } + for i := range elemCount { + elem := l.Get(Int(i)) + nativeElemVal, err := elem.ConvertToNative(otherElemType) + if err != nil { + return nil, err + } + nativeList.Index(i).Set(reflect.ValueOf(nativeElemVal)) + } + return nativeList.Interface(), nil +} + +// ConvertToType implements the ref.Val interface method. +func (l *baseList) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case ListType: + return l + case TypeType: + return ListType + } + return NewErr("type conversion error from '%s' to '%s'", ListType, typeVal) +} + +// Equal implements the ref.Val interface method. +func (l *baseList) Equal(other ref.Val) ref.Val { + otherList, ok := other.(traits.Lister) + if !ok { + return False + } + if l.Size() != otherList.Size() { + return False + } + for i := IntZero; i < l.Size().(Int); i++ { + thisElem := l.Get(i) + otherElem := otherList.Get(i) + elemEq := Equal(thisElem, otherElem) + if elemEq == False { + return False + } + } + return True +} + +// Get implements the traits.Indexer interface method. +func (l *baseList) Get(index ref.Val) ref.Val { + ind, err := GetOrError(index) + if err != nil { + return ValOrErr(index, "%v", err) + } + if ind < 0 || ind >= l.size { + return NewErr("index '%d' out of range in list size '%d'", ind, l.Size()) + } + return l.NativeToValue(l.get(ind)) +} + +// IsZeroValue returns true if the list is empty. +func (l *baseList) IsZeroValue() bool { + return l.size == 0 +} + +// Fold calls the FoldEntry method for each (index, value) pair in the list. +func (l *baseList) Fold(f traits.Folder) { + for i := 0; i < l.size; i++ { + if !f.FoldEntry(i, l.get(i)) { + break + } + } +} + +// Iterator implements the traits.Iterable interface method. +func (l *baseList) Iterator() traits.Iterator { + return newListIterator(l) +} + +// Size implements the traits.Sizer interface method. +func (l *baseList) Size() ref.Val { + return Int(l.size) +} + +// AggregateSize implements the AggregateSizeVisitor interface method. +func (l *baseList) AggregateSize(sizer AggregateSizer) uint32 { + if sz := atomic.LoadUint32(&l.aggSize); sz != 0 { + return sz + } + var total uint32 + if l.value != nil { + if t, ok := getSliceElementsAggregateSize(sizer, l.value); ok { + total = t + } + } + if total == 0 { + total = uint32(1) + for i := range l.size { + total = safeAddUint32(total, sizer.AggregateSize(l.get(i))) + } + } + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&l.aggSize, total) + } + return total +} + +// Type implements the ref.Val interface method. +func (l *baseList) Type() ref.Type { + return ListType +} + +// Value implements the ref.Val interface method. +func (l *baseList) Value() any { + return l.value +} + +// String converts the list to a human readable string form. +func (l *baseList) String() string { + var sb strings.Builder + sb.WriteString("[") + for i := 0; i < l.size; i++ { + sb.WriteString(fmt.Sprintf("%v", l.get(i))) + if i != l.size-1 { + sb.WriteString(", ") + } + } + sb.WriteString("]") + return sb.String() +} + +func formatList(l traits.Lister, sb *strings.Builder) { + sb.WriteString("[") + n, _ := l.Size().(Int) + for i := 0; i < int(n); i++ { + formatTo(sb, l.Get(Int(i))) + if i != int(n)-1 { + sb.WriteString(", ") + } + } + sb.WriteString("]") +} + +func (l *baseList) format(sb *strings.Builder) { + formatList(l, sb) +} + +// mutableList aggregates values into its internal storage. For use with internal CEL variables only. +type mutableList struct { + *sliceList[ref.Val] +} + +// Add copies elements from the other list into the internal storage of the mutable list. +// The ref.Val returned by Add is the receiver. +func (l *mutableList) Add(other ref.Val) ref.Val { + switch otherList := other.(type) { + case *mutableList: + l.elems = append(l.elems, otherList.elems...) + atomic.StoreUint32(&l.aggSize, 0) + case traits.Lister: + for i := IntZero; i < otherList.Size().(Int); i++ { + l.elems = append(l.elems, otherList.Get(i)) + } + atomic.StoreUint32(&l.aggSize, 0) + default: + return MaybeNoSuchOverloadErr(otherList) + } + return l +} + +// ToImmutableList returns an immutable list based on the internal storage of the mutable list. +// It transfers ownership of the underlying buffer to the returned immutable list and clears +// internal elements to prevent subsequent modifications from aliasing. +func (l *mutableList) ToImmutableList() traits.Lister { + elems := l.elems + l.elems = nil + atomic.StoreUint32(&l.aggSize, 0) + return NewRefValList(l.Adapter, elems) +} + +// concatList combines two list implementations together into a view. +// The `Adapter` enables native type to CEL type conversions. +type concatList struct { + Adapter + value any + valueOnce sync.Once + prevList traits.Lister + nextList traits.Lister + cachedSize ref.Val + aggSize uint32 +} + +func newConcatList(adapter Adapter, prevList, nextList traits.Lister) ref.Val { + prevSize := prevList.Size().(Int) + nextSize := nextList.Size().(Int) + if prevSize == IntZero { + return nextList.(ref.Val) + } + if nextSize == IntZero { + return prevList.(ref.Val) + } + return &concatList{ + Adapter: adapter, + prevList: prevList, + nextList: nextList, + cachedSize: prevSize.Add(nextSize), + } +} + +// Add implements the traits.Adder interface method. +func (l *concatList) Add(other ref.Val) ref.Val { + otherList, ok := other.(traits.Lister) + if !ok { + return MaybeNoSuchOverloadErr(other) + } + return newConcatList(l.Adapter, l, otherList) +} + +// Contains implements the traits.Container interface method. +func (l *concatList) Contains(elem ref.Val) ref.Val { + // The concat list relies on the IsErrorOrUnknown checks against the input element to be + // performed by the `prevList` and/or `nextList`. + prev := l.prevList.Contains(elem) + // Short-circuit the return if the elem was found in the prev list. + if prev == True { + return prev + } + // Return if the elem was found in the next list. + next := l.nextList.Contains(elem) + if next == True { + return next + } + // Handle the case where an error or unknown was encountered before checking next. + if IsUnknownOrError(prev) { + return prev + } + // Otherwise, rely on the next value as the representative result. + return next +} + +// ConvertToNative implements the ref.Val interface method. +func (l *concatList) ConvertToNative(typeDesc reflect.Type) (any, error) { + combined := NewDynamicList(l.Adapter, l.Value()) + return combined.ConvertToNative(typeDesc) +} + +// ConvertToType implements the ref.Val interface method. +func (l *concatList) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case ListType: + return l + case TypeType: + return ListType + } + return NewErr("type conversion error from '%s' to '%s'", ListType, typeVal) +} + +// Equal implements the ref.Val interface method. +func (l *concatList) Equal(other ref.Val) ref.Val { + otherList, ok := other.(traits.Lister) + if !ok { + return False + } + if l.Size() != otherList.Size() { + return False + } + var maybeErr ref.Val + for i := IntZero; i < l.Size().(Int); i++ { + thisElem := l.Get(i) + otherElem := otherList.Get(i) + elemEq := Equal(thisElem, otherElem) + if elemEq == False { + return False + } + if maybeErr == nil && IsUnknownOrError(elemEq) { + maybeErr = elemEq + } + } + if maybeErr != nil { + return maybeErr + } + return True +} + +// Get implements the traits.Indexer interface method. +func (l *concatList) Get(index ref.Val) ref.Val { + ind, err := GetOrError(index) + if err != nil { + return ValOrErr(index, "%v", err) + } + i := Int(ind) + if i < l.prevList.Size().(Int) { + return l.prevList.Get(i) + } + offset := i - l.prevList.Size().(Int) + return l.nextList.Get(offset) +} + +// IsZeroValue returns true if the list is empty. +func (l *concatList) IsZeroValue() bool { + return l.Size().(Int) == 0 +} + +// Fold calls the FoldEntry method for each (index, value) pair in the list. +func (l *concatList) Fold(f traits.Folder) { + for i := Int(0); i < l.Size().(Int); i++ { + if !f.FoldEntry(i, l.Get(i)) { + break + } + } +} + +// Iterator implements the traits.Iterable interface method. +func (l *concatList) Iterator() traits.Iterator { + return newListIterator(l) +} + +// Size implements the traits.Sizer interface method. +func (l *concatList) Size() ref.Val { + return l.cachedSize +} + +// AggregateSize implements the AggregateSizeVisitor interface method. +func (l *concatList) AggregateSize(sizer AggregateSizer) uint32 { + if sz := atomic.LoadUint32(&l.aggSize); sz != 0 { + return sz + } + total := safeAddUint32(sizer.AggregateSize(l.prevList), sizer.AggregateSize(l.nextList)) + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&l.aggSize, total) + } + return total +} + +// String converts the concatenated list to a human-readable string. +func (l *concatList) String() string { + var sb strings.Builder + sb.WriteString("[") + for i := Int(0); i < l.Size().(Int); i++ { + sb.WriteString(fmt.Sprintf("%v", l.Get(i))) + if i != l.Size().(Int)-1 { + sb.WriteString(", ") + } + } + sb.WriteString("]") + return sb.String() +} + +// Type implements the ref.Val interface method. +func (l *concatList) Type() ref.Type { + return ListType +} + +// Value implements the ref.Val interface method. +func (l *concatList) Value() any { + l.valueOnce.Do(func() { + merged := make([]ref.Val, l.Size().(Int)) + prevLen := l.prevList.Size().(Int) + for i := Int(0); i < prevLen; i++ { + merged[i] = l.prevList.Get(i) + } + nextLen := l.nextList.Size().(Int) + for j := Int(0); j < nextLen; j++ { + merged[prevLen+j] = l.nextList.Get(j) + } + l.value = merged + }) + return l.value +} + +func newListIterator(listValue traits.Lister) traits.Iterator { + return &listIterator{ + listValue: listValue, + len: listValue.Size().(Int), + } +} + +type listIterator struct { + *baseIterator + listValue traits.Lister + cursor Int + len Int +} + +// HasNext implements the traits.Iterator interface method. +func (it *listIterator) HasNext() ref.Val { + return Bool(it.cursor < it.len) +} + +// Next implements the traits.Iterator interface method. +func (it *listIterator) Next() ref.Val { + if it.HasNext() == True { + index := it.cursor + it.cursor++ + return it.listValue.Get(index) + } + return nil +} + +// GetOrError converts an input index value into either a lossless integer index or an error. +func GetOrError(index ref.Val) (int, error) { + switch iv := index.(type) { + case Int: + return int(iv), nil + case Double: + if ik, ok := doubleToInt64Lossless(float64(iv)); ok { + return int(ik), nil + } + return -1, fmt.Errorf("unsupported index value %v in list", index) + case Uint: + if ik, ok := uint64ToInt64Lossless(uint64(iv)); ok { + return int(ik), nil + } + return -1, fmt.Errorf("unsupported index value %v in list", index) + default: + return -1, fmt.Errorf("unsupported index type '%s' in list", index.Type()) + } +} + +// ToFoldableList will create a Foldable version of a list suitable for key-value pair iteration. +// +// For values which are already Foldable, this call is a no-op. For all other values, the fold is +// driven via the Size() and Get() calls which means that the folding will function, but take a +// performance hit. +func ToFoldableList(l traits.Lister) traits.Foldable { + if f, ok := l.(traits.Foldable); ok { + return f + } + return interopFoldableList{Lister: l} +} + +type interopFoldableList struct { + traits.Lister +} + +// Fold implements the traits.Foldable interface method and performs an iteration over the +// range of elements of the list. +func (l interopFoldableList) Fold(f traits.Folder) { + sz := l.Size().(Int) + for i := Int(0); i < sz; i++ { + if !f.FoldEntry(i, l.Get(i)) { + break + } + } +} + +type sliceList[T any] struct { + Adapter + elems []T + elemTypePtr unsafe.Pointer + meta *dynamicSliceMeta + aggSize uint32 + qualifyRawVal bool + isNilSlice bool +} + +func (l *sliceList[T]) len() int { + if l.meta != nil && l.meta.elemStride > 0 { + return len(l.elems) / int(l.meta.elemStride) + } + return len(l.elems) +} + +func (l *sliceList[T]) nativeSliceValue() any { + if l.meta != nil { + n := l.len() + var data unsafe.Pointer + if n > 0 { + data = unsafe.Pointer(unsafe.SliceData(l.elems)) + } else if !l.isNilSlice { + data = unsafe.Pointer(&emptySliceData) + } + hdr := &unsafeSlice{ + Data: data, + Len: n, + Cap: n, + } + return packAny(l.meta.sliceTypePtr, unsafe.Pointer(hdr)) + } + return l.elems +} + +func (l *sliceList[T]) rawElemAt(ind int) (any, bool) { + switch s := any(l).(type) { + case *sliceList[unsafe.Pointer]: + if s.elemTypePtr != nil { + ptr := s.elems[ind] + if ptr == nil { + return NullValue, true + } + return packAny(s.elemTypePtr, ptr), true + } + case *sliceList[uint8]: + if s.meta != nil && s.meta.elemStride > 0 && s.elemTypePtr != nil { + return packAny(s.elemTypePtr, unsafe.Pointer(&s.elems[uintptr(ind)*s.meta.elemStride])), true + } + } + if l.elemTypePtr != nil { + return packAny(l.elemTypePtr, unsafe.Pointer(&l.elems[ind])), true + } + return nil, false +} + +func (l *sliceList[T]) getElem(ind int) ref.Val { + switch s := any(l).(type) { + case *sliceList[string]: + return String(s.elems[ind]) + case *sliceList[ref.Val]: + return s.elems[ind] + case *sliceList[int]: + return Int(s.elems[ind]) + case *sliceList[int64]: + return Int(s.elems[ind]) + case *sliceList[int32]: + return Int(s.elems[ind]) + case *sliceList[uint]: + return Uint(s.elems[ind]) + case *sliceList[uint64]: + return Uint(s.elems[ind]) + case *sliceList[uint32]: + return Uint(s.elems[ind]) + case *sliceList[uint8]: + if s.meta == nil { + return Uint(s.elems[ind]) + } + case *sliceList[float64]: + return Double(s.elems[ind]) + case *sliceList[float32]: + return Double(s.elems[ind]) + case *sliceList[bool]: + if s.elems[ind] { + return True + } + return False + case *sliceList[[]byte]: + return Bytes(s.elems[ind]) + } + if raw, ok := l.rawElemAt(ind); ok { + return l.Adapter.NativeToValue(raw) + } + return l.Adapter.NativeToValue(l.elems[ind]) +} + +// GetInt64Index returns the element at ind for fast index qualification in the interpreter. +// For non-proto Go struct elements, it returns the unboxed struct pointer or zero-copy struct +// emptyInterface so subsequent field accesses can read offsets without allocating a nativeObj. +func (l *sliceList[T]) GetInt64Index(ind int64) (any, bool) { + n := l.len() + if ind < 0 || ind >= int64(n) { + return nil, false + } + i := int(ind) + if !l.qualifyRawVal { + return l.getElem(i), true + } + if raw, ok := l.rawElemAt(i); ok { + return raw, true + } + return l.elems[i], true +} + +func (l *sliceList[T]) Add(other ref.Val) ref.Val { + otherList, ok := other.(traits.Lister) + if !ok { + return MaybeNoSuchOverloadErr(other) + } + return newConcatList(l.Adapter, l, otherList) +} + +func (l *sliceList[T]) Contains(elem ref.Val) ref.Val { + switch s := any(l).(type) { + case *sliceList[string]: + if strVal, ok := elem.(String); ok { + str := string(strVal) + for _, e := range s.elems { + if e == str { + return True + } + } + return False + } + case *sliceList[int64]: + if intVal, ok := elem.(Int); ok { + iv := int64(intVal) + for _, e := range s.elems { + if e == iv { + return True + } + } + return False + } + case *sliceList[int]: + if intVal, ok := elem.(Int); ok { + iv := int(intVal) + for _, e := range s.elems { + if e == iv { + return True + } + } + return False + } + case *sliceList[int32]: + if intVal, ok := elem.(Int); ok { + iv := int32(intVal) + if int64(iv) == int64(intVal) { + for _, e := range s.elems { + if e == iv { + return True + } + } + } + return False + } + case *sliceList[uint64]: + if uintVal, ok := elem.(Uint); ok { + uv := uint64(uintVal) + for _, e := range s.elems { + if e == uv { + return True + } + } + return False + } + case *sliceList[uint]: + if uintVal, ok := elem.(Uint); ok { + uv := uint(uintVal) + for _, e := range s.elems { + if e == uv { + return True + } + } + return False + } + case *sliceList[uint32]: + if uintVal, ok := elem.(Uint); ok { + uv := uint32(uintVal) + if uint64(uv) == uint64(uintVal) { + for _, e := range s.elems { + if e == uv { + return True + } + } + } + return False + } + case *sliceList[float64]: + if dblVal, ok := elem.(Double); ok { + dv := float64(dblVal) + for _, e := range s.elems { + if e == dv { + return True + } + } + return False + } + case *sliceList[bool]: + if boolVal, ok := elem.(Bool); ok { + bv := bool(boolVal) + for _, e := range s.elems { + if e == bv { + return True + } + } + return False + } + } + n := l.len() + for i := 0; i < n; i++ { + cmp := elem.Equal(l.getElem(i)) + if b, ok := cmp.(Bool); ok && b == True { + return True + } + } + return False +} + +func (l *sliceList[T]) ConvertToNative(typeDesc reflect.Type) (any, error) { + if l.meta == nil && typeDesc == reflect.TypeFor[[]T]() { + return l.elems, nil + } + val := l.nativeSliceValue() + if l.meta != nil && l.meta.sliceType == typeDesc { + return val, nil + } + return convertListToNative(l, val, typeDesc) +} + +func (l *sliceList[T]) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case ListType: + return l + case TypeType: + return ListType + } + return NewErr("type conversion error from '%s' to '%s'", ListType, typeVal) +} + +func (l *sliceList[T]) Equal(other ref.Val) ref.Val { + otherList, ok := other.(traits.Lister) + if !ok { + return False + } + if l.Size() != otherList.Size() { + return False + } + n := l.len() + if otherSlice, ok := other.(*sliceList[T]); ok && l.meta == otherSlice.meta { + switch s := any(l).(type) { + case *sliceList[string]: + o := any(otherSlice).(*sliceList[string]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[int64]: + o := any(otherSlice).(*sliceList[int64]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[int]: + o := any(otherSlice).(*sliceList[int]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[int32]: + o := any(otherSlice).(*sliceList[int32]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[uint64]: + o := any(otherSlice).(*sliceList[uint64]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[uint]: + o := any(otherSlice).(*sliceList[uint]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[uint32]: + o := any(otherSlice).(*sliceList[uint32]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[float64]: + o := any(otherSlice).(*sliceList[float64]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[float32]: + o := any(otherSlice).(*sliceList[float32]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[bool]: + o := any(otherSlice).(*sliceList[bool]) + for i := range n { + if s.elems[i] != o.elems[i] { + return False + } + } + return True + case *sliceList[[]byte]: + o := any(otherSlice).(*sliceList[[]byte]) + for i := range n { + if !bytes.Equal(s.elems[i], o.elems[i]) { + return False + } + } + return True + } + for i := range n { + if Equal(l.getElem(i), otherSlice.getElem(i)) == False { + return False + } + } + return True + } + for i := range n { + thisElem := l.getElem(i) + otherElem := otherList.Get(Int(i)) + if Equal(thisElem, otherElem) == False { + return False + } + } + return True +} + +func (l *sliceList[T]) Get(index ref.Val) ref.Val { + ind, err := GetOrError(index) + if err != nil { + return ValOrErr(index, "%v", err) + } + n := l.len() + if ind < 0 || ind >= n { + return NewErr("index '%d' out of range in list size '%d'", ind, n) + } + return l.getElem(ind) +} + +func (l *sliceList[T]) IsZeroValue() bool { + return l.len() == 0 +} + +func (l *sliceList[T]) Fold(f traits.Folder) { + switch s := any(l).(type) { + case *sliceList[string]: + for i, e := range s.elems { + if !f.FoldEntry(i, String(e)) { + break + } + } + case *sliceList[ref.Val]: + for i, e := range s.elems { + if !f.FoldEntry(i, e) { + break + } + } + case *sliceList[int]: + for i, e := range s.elems { + if !f.FoldEntry(i, Int(e)) { + break + } + } + case *sliceList[int64]: + for i, e := range s.elems { + if !f.FoldEntry(i, Int(e)) { + break + } + } + case *sliceList[int32]: + for i, e := range s.elems { + if !f.FoldEntry(i, Int(e)) { + break + } + } + case *sliceList[uint]: + for i, e := range s.elems { + if !f.FoldEntry(i, Uint(e)) { + break + } + } + case *sliceList[uint64]: + for i, e := range s.elems { + if !f.FoldEntry(i, Uint(e)) { + break + } + } + case *sliceList[uint32]: + for i, e := range s.elems { + if !f.FoldEntry(i, Uint(e)) { + break + } + } + case *sliceList[uint8]: + if s.meta != nil && s.meta.elemStride > 0 && s.elemTypePtr != nil { + stride := s.meta.elemStride + n := len(s.elems) / int(stride) + for i := 0; i < n; i++ { + if !f.FoldEntry(i, packAny(s.elemTypePtr, unsafe.Pointer(&s.elems[uintptr(i)*stride]))) { + break + } + } + return + } + for i, e := range s.elems { + if !f.FoldEntry(i, Uint(e)) { + break + } + } + case *sliceList[float64]: + for i, e := range s.elems { + if !f.FoldEntry(i, Double(e)) { + break + } + } + case *sliceList[float32]: + for i, e := range s.elems { + if !f.FoldEntry(i, Double(e)) { + break + } + } + case *sliceList[bool]: + for i, e := range s.elems { + val := False + if e { + val = True + } + if !f.FoldEntry(i, val) { + break + } + } + case *sliceList[[]byte]: + for i, e := range s.elems { + if !f.FoldEntry(i, Bytes(e)) { + break + } + } + case *sliceList[unsafe.Pointer]: + if s.elemTypePtr != nil { + for i, ptr := range s.elems { + if ptr == nil { + if !f.FoldEntry(i, NullValue) { + break + } + continue + } + if !f.FoldEntry(i, packAny(s.elemTypePtr, ptr)) { + break + } + } + return + } + default: + if l.elemTypePtr != nil { + for i := range l.elems { + if !f.FoldEntry(i, packAny(l.elemTypePtr, unsafe.Pointer(&l.elems[i]))) { + break + } + } + return + } + for i, e := range l.elems { + if !f.FoldEntry(i, e) { + break + } + } + } +} + +func (l *sliceList[T]) Iterator() traits.Iterator { + return &sliceListIterator[T]{ + list: l, + len: l.len(), + } +} + +func (l *sliceList[T]) Size() ref.Val { + return Int(l.len()) +} + +func (l *sliceList[T]) AggregateSize(sizer AggregateSizer) uint32 { + if sz := atomic.LoadUint32(&l.aggSize); sz != 0 { + return sz + } + var total uint32 + if l.meta == nil && l.elemTypePtr == nil { + if t, ok := getSliceElementsAggregateSize(sizer, l.elems); ok { + total = t + } + } + if total == 0 { + total = uint32(1) + n := l.len() + for i := 0; i < n; i++ { + total = safeAddUint32(total, sizer.AggregateSize(l.getElem(i))) + } + } + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&l.aggSize, total) + } + return total +} + +func (l *sliceList[T]) Type() ref.Type { + return ListType +} + +func (l *sliceList[T]) Value() any { + return l.nativeSliceValue() +} + +func (l *sliceList[T]) String() string { + var sb strings.Builder + sb.WriteString("[") + n := l.len() + for i := 0; i < n; i++ { + if l.meta != nil { + sb.WriteString(fmt.Sprintf("%v", l.getElem(i).Value())) + } else { + sb.WriteString(fmt.Sprintf("%v", l.elems[i])) + } + if i != n-1 { + sb.WriteString(", ") + } + } + sb.WriteString("]") + return sb.String() +} + +func (l *sliceList[T]) format(sb *strings.Builder) { + formatList(l, sb) +} + +type sliceListIterator[T any] struct { + *baseIterator + list *sliceList[T] + cursor int + len int +} + +func (it *sliceListIterator[T]) HasNext() ref.Val { + if it.cursor < it.len { + return True + } + return False +} + +func (it *sliceListIterator[T]) Next() ref.Val { + if it.cursor < it.len { + ind := it.cursor + it.cursor++ + return it.list.getElem(ind) + } + return nil +} + +var emptySliceData byte diff --git a/vendor/github.com/google/cel-go/common/types/map.go b/vendor/cel.dev/cel-go/common/types/map.go similarity index 61% rename from vendor/github.com/google/cel-go/common/types/map.go rename to vendor/cel.dev/cel-go/common/types/map.go index e4d6f765745..3822056171a 100644 --- a/vendor/github.com/google/cel-go/common/types/map.go +++ b/vendor/cel.dev/cel-go/common/types/map.go @@ -16,17 +16,21 @@ package types import ( "fmt" + "maps" + "math" "reflect" "sort" "strings" + "sync/atomic" "unicode" + "unsafe" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/google/cel-go/common/types/pb" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/types/pb" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" @@ -43,6 +47,16 @@ func NewDynamicMap(adapter Adapter, value any) traits.Mapper { } } +// NewMap returns a traits.Mapper backed by a typed Go map[K]V. +func NewMap[K comparable, V any](adapter Adapter, value map[K]V) traits.Mapper { + return &nativeMap[K, V]{ + Adapter: adapter, + mapVal: value, + valTypePtr: elemTypePtrFor[V](), + qualifyRawVal: isQualifyRawStruct[V](), + } +} + // NewJSONStruct creates a traits.Mapper implementation backed by a JSON struct that has been // encoded in protocol buffer form. // @@ -59,32 +73,17 @@ func NewJSONStruct(adapter Adapter, value *structpb.Struct) traits.Mapper { // NewRefValMap returns a specialized traits.Mapper with CEL valued keys and values. func NewRefValMap(adapter Adapter, value map[ref.Val]ref.Val) traits.Mapper { - return &baseMap{ - Adapter: adapter, - mapAccessor: newRefValMapAccessor(value), - value: value, - size: len(value), - } + return NewMap(adapter, value) } // NewStringInterfaceMap returns a specialized traits.Mapper with string keys and interface values. func NewStringInterfaceMap(adapter Adapter, value map[string]any) traits.Mapper { - return &baseMap{ - Adapter: adapter, - mapAccessor: newStringIfaceMapAccessor(adapter, value), - value: value, - size: len(value), - } + return NewMap(adapter, value) } // NewStringStringMap returns a specialized traits.Mapper with string keys and values. func NewStringStringMap(adapter Adapter, value map[string]string) traits.Mapper { - return &baseMap{ - Adapter: adapter, - mapAccessor: newStringMapAccessor(value), - value: value, - size: len(value), - } + return NewMap(adapter, value) } // NewProtoMap returns a specialized traits.Mapper for handling protobuf map values. @@ -101,16 +100,12 @@ func NewMutableMap(adapter Adapter, mutableValues map[ref.Val]ref.Val) traits.Mu for k, v := range mutableValues { mutableCopy[k] = v } - m := &mutableMap{ - baseMap: &baseMap{ - Adapter: adapter, - mapAccessor: newRefValMapAccessor(mutableCopy), - value: mutableCopy, - size: len(mutableCopy), + return &mutableMap{ + nativeMap: &nativeMap[ref.Val, ref.Val]{ + Adapter: adapter, + mapVal: mutableCopy, }, - mutableValues: mutableCopy, } - return m } // mapAccessor is a private interface for finding values within a map and iterating over the keys. @@ -142,8 +137,13 @@ type baseMap struct { // value is the native Go value upon which the map type operators. value any - // size is the number of entries in the map. size int + + // aggSize memoizes the aggregate size computed by the first completed sizing of this + // map. Accessed atomically since immutable maps may be shared across concurrent + // evaluations; zero means not yet computed. See the SizeCalculator documentation for + // the memoization contract. + aggSize uint32 } // Contains implements the traits.Container interface method. @@ -154,13 +154,17 @@ func (m *baseMap) Contains(index ref.Val) ref.Val { // ConvertToNative implements the ref.Val interface method. func (m *baseMap) ConvertToNative(typeDesc reflect.Type) (any, error) { + return convertMapToNative(m, m.value, typeDesc) +} + +func convertMapToNative(m traits.Mapper, val any, typeDesc reflect.Type) (any, error) { // If the map is already assignable to the desired type return it, e.g. interfaces and // maps with the same key value types. if typeDesc == reflect.TypeFor[any]() { typeDesc = reflect.TypeFor[map[any]any]() } - if reflect.TypeOf(m.value).AssignableTo(typeDesc) { - return m.value, nil + if val != nil && reflect.TypeOf(val).AssignableTo(typeDesc) { + return val, nil } if reflect.TypeOf(m).AssignableTo(typeDesc) { return m, nil @@ -200,7 +204,7 @@ func (m *baseMap) ConvertToNative(typeDesc reflect.Type) (any, error) { case reflect.Map: otherKey := typeDesc.Key() otherElem := typeDesc.Elem() - nativeMap := reflect.MakeMapWithSize(typeDesc, m.size) + nativeMap := reflect.MakeMapWithSize(typeDesc, int(m.Size().(Int))) it := m.Iterator() for it.HasNext() == True { key := it.Next() @@ -303,6 +307,28 @@ func (m *baseMap) Size() ref.Val { return Int(m.size) } +// AggregateSize implements the AggregateSizeVisitor interface method. +func (m *baseMap) AggregateSize(sizer AggregateSizer) uint32 { + if sz := atomic.LoadUint32(&m.aggSize); sz != 0 { + return sz + } + var total uint32 + if m.value != nil { + if t, ok := getMapElementsAggregateSize(sizer, m.value); ok { + total = t + } + } + if total == 0 { + f := foldableAggregateSizer{sizer: sizer, total: 1} + m.Fold(&f) + total = f.total + } + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&m.aggSize, total) + } + return total +} + // String converts the map into a human-readable string. func (m *baseMap) String() string { var sb strings.Builder @@ -312,7 +338,7 @@ func (m *baseMap) String() string { for it.HasNext() == True { k := it.Next() v, _ := m.Find(k) - sb.WriteString(fmt.Sprintf("%v: %v", k, v)) + fmt.Fprintf(&sb, "%v: %v", k, v) if i != m.size-1 { sb.WriteString(", ") } @@ -369,8 +395,7 @@ func (m *baseMap) Value() any { // mutableMap holds onto a set of mutable values which are used for intermediate computations. type mutableMap struct { - *baseMap - mutableValues map[ref.Val]ref.Val + *nativeMap[ref.Val, ref.Val] } // Insert implements the traits.MutableMapper interface method, returning true if the key insertion @@ -379,14 +404,17 @@ func (m *mutableMap) Insert(k, v ref.Val) ref.Val { if _, found := m.Find(k); found { return NewErr("insert failed: key %v already exists", k) } - m.mutableValues[k] = v + m.mapVal[k] = v + atomic.StoreUint32(&m.aggSize, 0) return m } // ToImmutableMap implements the traits.MutableMapper interface method, converting a mutable map // an immutable map implementation. func (m *mutableMap) ToImmutableMap() traits.Mapper { - return NewRefValMap(m.Adapter, m.mutableValues) + copyMap := make(map[ref.Val]ref.Val, len(m.mapVal)) + maps.Copy(copyMap, m.mapVal) + return NewRefValMap(m.Adapter, copyMap) } func newJSONStructAccessor(adapter Adapter, st map[string]*structpb.Value) mapAccessor { @@ -525,171 +553,565 @@ func (m *reflectMapAccessor) Fold(f traits.Folder) { } } -func newRefValMapAccessor(mapVal map[ref.Val]ref.Val) mapAccessor { - return &refValMapAccessor{mapVal: mapVal} +func toInt64Key(key ref.Val) (int64, bool) { + switch k := key.(type) { + case Int: + return int64(k), true + case Uint: + return uint64ToInt64Lossless(uint64(k)) + case Double: + return doubleToInt64Lossless(float64(k)) + default: + return 0, false + } } -type refValMapAccessor struct { - mapVal map[ref.Val]ref.Val +func toUint64Key(key ref.Val) (uint64, bool) { + switch k := key.(type) { + case Uint: + return uint64(k), true + case Int: + return int64ToUint64Lossless(int64(k)) + case Double: + return doubleToUint64Lossless(float64(k)) + default: + return 0, false + } } -// Find uses native map accesses to find the key, returning (value, true) if present. -// -// If the key is not found the function returns (nil, false). -func (a *refValMapAccessor) Find(key ref.Val) (ref.Val, bool) { - if len(a.mapVal) == 0 { +type nativeMap[K comparable, V any] struct { + Adapter + mapVal map[K]V + valTypePtr unsafe.Pointer + qualifyRawVal bool + aggSize uint32 +} + +func (m *nativeMap[K, V]) keyToRefVal(k K) ref.Val { + switch any((*K)(nil)).(type) { + case *string: + return String(*(*string)(unsafe.Pointer(&k))) + case *ref.Val: + return *(*ref.Val)(unsafe.Pointer(&k)) + case *int64: + return Int(*(*int64)(unsafe.Pointer(&k))) + case *int: + return Int(*(*int)(unsafe.Pointer(&k))) + case *int32: + return Int(*(*int32)(unsafe.Pointer(&k))) + case *uint64: + return Uint(*(*uint64)(unsafe.Pointer(&k))) + case *uint: + return Uint(*(*uint)(unsafe.Pointer(&k))) + case *uint32: + return Uint(*(*uint32)(unsafe.Pointer(&k))) + case *bool: + if *(*bool)(unsafe.Pointer(&k)) { + return True + } + return False + default: + return m.Adapter.NativeToValue(k) + } +} + +func (m *nativeMap[K, V]) valToRefVal(v V) ref.Val { + switch any((*V)(nil)).(type) { + case *string: + return String(*(*string)(unsafe.Pointer(&v))) + case *ref.Val: + return *(*ref.Val)(unsafe.Pointer(&v)) + case *int64: + return Int(*(*int64)(unsafe.Pointer(&v))) + case *int: + return Int(*(*int)(unsafe.Pointer(&v))) + case *int32: + return Int(*(*int32)(unsafe.Pointer(&v))) + case *uint64: + return Uint(*(*uint64)(unsafe.Pointer(&v))) + case *uint: + return Uint(*(*uint)(unsafe.Pointer(&v))) + case *uint32: + return Uint(*(*uint32)(unsafe.Pointer(&v))) + case *float64: + return Double(*(*float64)(unsafe.Pointer(&v))) + case *float32: + return Double(*(*float32)(unsafe.Pointer(&v))) + case *bool: + if *(*bool)(unsafe.Pointer(&v)) { + return True + } + return False + case *[]byte: + return Bytes(*(*[]byte)(unsafe.Pointer(&v))) + default: + if m.valTypePtr != nil { + return m.Adapter.NativeToValue(packAny(m.valTypePtr, unsafe.Pointer(heapCopy(v)))) + } + return m.Adapter.NativeToValue(v) + } +} + +func (m *nativeMap[K, V]) rawValToAny(v V) any { + if m.valTypePtr != nil { + return packAny(m.valTypePtr, unsafe.Pointer(heapCopy(v))) + } + if m.qualifyRawVal && anyDataPtr(v) == nil { + return NullValue + } + return v +} + +func (m *nativeMap[K, V]) keyToFoldAny(k K) any { + switch any((*K)(nil)).(type) { + case *string: + return String(*(*string)(unsafe.Pointer(&k))) + case *ref.Val: + return *(*ref.Val)(unsafe.Pointer(&k)) + case *int64: + return Int(*(*int64)(unsafe.Pointer(&k))) + case *int: + return Int(*(*int)(unsafe.Pointer(&k))) + case *int32: + return Int(*(*int32)(unsafe.Pointer(&k))) + case *uint64: + return Uint(*(*uint64)(unsafe.Pointer(&k))) + case *uint: + return Uint(*(*uint)(unsafe.Pointer(&k))) + case *uint32: + return Uint(*(*uint32)(unsafe.Pointer(&k))) + case *bool: + if *(*bool)(unsafe.Pointer(&k)) { + return True + } + return False + default: + return k + } +} + +func (m *nativeMap[K, V]) valToFoldAny(v V) any { + switch any((*V)(nil)).(type) { + case *string: + return String(*(*string)(unsafe.Pointer(&v))) + case *ref.Val: + return *(*ref.Val)(unsafe.Pointer(&v)) + case *int64: + return Int(*(*int64)(unsafe.Pointer(&v))) + case *int: + return Int(*(*int)(unsafe.Pointer(&v))) + case *int32: + return Int(*(*int32)(unsafe.Pointer(&v))) + case *uint64: + return Uint(*(*uint64)(unsafe.Pointer(&v))) + case *uint: + return Uint(*(*uint)(unsafe.Pointer(&v))) + case *uint32: + return Uint(*(*uint32)(unsafe.Pointer(&v))) + case *float64: + return Double(*(*float64)(unsafe.Pointer(&v))) + case *float32: + return Double(*(*float32)(unsafe.Pointer(&v))) + case *bool: + if *(*bool)(unsafe.Pointer(&v)) { + return True + } + return False + case *[]byte: + return Bytes(*(*[]byte)(unsafe.Pointer(&v))) + default: + return m.rawValToAny(v) + } +} + +func (m *nativeMap[K, V]) Find(key ref.Val) (ref.Val, bool) { + v, found := m.findVal(key) + if !found { return nil, false } - if keyVal, found := a.mapVal[key]; found { - return keyVal, true + return m.valToRefVal(v), true +} + +// findVal resolves key to the stored native value without converting it to a ref.Val, so that +// presence tests (`in`, has(), optional selection) do not pay for value conversion. +func (m *nativeMap[K, V]) findVal(key ref.Val) (v V, found bool) { + if len(m.mapVal) == 0 { + return v, false } - switch k := key.(type) { + var k K + switch any((*K)(nil)).(type) { + case *string: + sk, ok := key.(String) + if !ok { + return v, false + } + k = *(*K)(unsafe.Pointer(&sk)) + case *int64: + ik, ok := toInt64Key(key) + if !ok { + return v, false + } + k = *(*K)(unsafe.Pointer(&ik)) + case *int: + ik, ok := toInt64Key(key) + if !ok || ik < math.MinInt || ik > math.MaxInt { + return v, false + } + iv := int(ik) + k = *(*K)(unsafe.Pointer(&iv)) + case *int32: + ik, ok := toInt64Key(key) + if !ok || ik < math.MinInt32 || ik > math.MaxInt32 { + return v, false + } + iv := int32(ik) + k = *(*K)(unsafe.Pointer(&iv)) + case *uint64: + uk, ok := toUint64Key(key) + if !ok { + return v, false + } + k = *(*K)(unsafe.Pointer(&uk)) + case *uint: + uk, ok := toUint64Key(key) + if !ok || uk > math.MaxUint { + return v, false + } + uv := uint(uk) + k = *(*K)(unsafe.Pointer(&uv)) + case *uint32: + uk, ok := toUint64Key(key) + if !ok || uk > math.MaxUint32 { + return v, false + } + uv := uint32(uk) + k = *(*K)(unsafe.Pointer(&uv)) + case *bool: + bk, ok := key.(Bool) + if !ok { + return v, false + } + bv := bool(bk) + k = *(*K)(unsafe.Pointer(&bv)) + case *ref.Val: + return m.findRefValKey(key) + default: + rawK, err := key.ConvertToNative(reflect.TypeFor[K]()) + if err != nil { + return v, false + } + typedK, ok := rawK.(K) + if !ok { + return v, false + } + k = typedK + } + v, found = m.mapVal[k] + return v, found +} + +// findRefValKey looks up a key in a map whose key type is ref.Val, falling back to numerically +// equivalent keys of other numeric types (e.g. 1.0 -> 1 -> 1u) when there is no exact match. +func (m *nativeMap[K, V]) findRefValKey(key ref.Val) (v V, found bool) { + if v, found = m.lookupRefVal(key); found { + return v, true + } + switch kv := key.(type) { case Double: - if ik, ok := doubleToInt64Lossless(float64(k)); ok { - if keyVal, found := a.mapVal[Int(ik)]; found { - return keyVal, found + if ik, ok := doubleToInt64Lossless(float64(kv)); ok { + if v, found = m.lookupRefVal(Int(ik)); found { + return v, true } } - if uk, ok := doubleToUint64Lossless(float64(k)); ok { - keyVal, found := a.mapVal[Uint(uk)] - return keyVal, found + if uk, ok := doubleToUint64Lossless(float64(kv)); ok { + return m.lookupRefVal(Uint(uk)) } - // map keys of type double are not supported. case Int: - if uk, ok := int64ToUint64Lossless(int64(k)); ok { - keyVal, found := a.mapVal[Uint(uk)] - return keyVal, found + if uk, ok := int64ToUint64Lossless(int64(kv)); ok { + return m.lookupRefVal(Uint(uk)) } case Uint: - if ik, ok := uint64ToInt64Lossless(uint64(k)); ok { - keyVal, found := a.mapVal[Int(ik)] - return keyVal, found + if ik, ok := uint64ToInt64Lossless(uint64(kv)); ok { + return m.lookupRefVal(Int(ik)) } } + return v, false +} + +// lookupRefVal performs a direct map lookup for maps whose key type K is ref.Val. +func (m *nativeMap[K, V]) lookupRefVal(rk ref.Val) (V, bool) { + v, found := m.mapVal[*(*K)(unsafe.Pointer(&rk))] + return v, found +} + +func (m *nativeMap[K, V]) valToQualifyAny(v V) any { + if m.qualifyRawVal { + return m.rawValToAny(v) + } + return m.valToRefVal(v) +} + +func (m *nativeMap[K, V]) FindStringKey(s string) (any, bool) { + switch any((*K)(nil)).(type) { + case *string: + v, found := m.mapVal[*(*K)(unsafe.Pointer(&s))] + if !found { + return nil, false + } + return m.valToQualifyAny(v), true + case *ref.Val: + var rk ref.Val = String(s) + v, found := m.mapVal[*(*K)(unsafe.Pointer(&rk))] + if !found { + return nil, false + } + return m.valToQualifyAny(v), true + default: + return nil, false + } +} + +func (m *nativeMap[K, V]) FindInt64Key(ik int64) (any, bool) { + switch any((*K)(nil)).(type) { + case *int64: + v, found := m.mapVal[*(*K)(unsafe.Pointer(&ik))] + if !found { + return nil, false + } + return m.valToQualifyAny(v), true + case *int: + if ik < math.MinInt || ik > math.MaxInt { + return nil, false + } + iv := int(ik) + v, found := m.mapVal[*(*K)(unsafe.Pointer(&iv))] + if !found { + return nil, false + } + return m.valToQualifyAny(v), true + case *int32: + if ik < math.MinInt32 || ik > math.MaxInt32 { + return nil, false + } + iv := int32(ik) + v, found := m.mapVal[*(*K)(unsafe.Pointer(&iv))] + if !found { + return nil, false + } + return m.valToQualifyAny(v), true + case *ref.Val: + var rk ref.Val = Int(ik) + v, found := m.mapVal[*(*K)(unsafe.Pointer(&rk))] + if found { + return m.valToQualifyAny(v), true + } + if ik >= 0 { + rk = Uint(ik) + v, found = m.mapVal[*(*K)(unsafe.Pointer(&rk))] + if found { + return m.valToQualifyAny(v), true + } + } + return nil, false + default: + return nil, false + } +} + +func (m *nativeMap[K, V]) FindNative(key any) (any, bool) { + switch k := key.(type) { + case String: + return m.FindStringKey(string(k)) + case string: + return m.FindStringKey(k) + case Int: + return m.FindInt64Key(int64(k)) + case int64: + return m.FindInt64Key(k) + case int: + return m.FindInt64Key(int64(k)) + } + if rv, ok := key.(ref.Val); ok { + return m.Find(rv) + } return nil, false } -// Iterator produces a new traits.Iterator which iterates over the map keys via Golang reflection. -func (a *refValMapAccessor) Iterator() traits.Iterator { - return &mapIterator{ - Adapter: DefaultTypeAdapter, - mapKeys: reflect.ValueOf(a.mapVal).MapRange(), - len: len(a.mapVal), - } +func (m *nativeMap[K, V]) Contains(index ref.Val) ref.Val { + _, found := m.findVal(index) + return Bool(found) } -// Fold calls the FoldEntry method for each (key, value) pair in the map. -func (a *refValMapAccessor) Fold(f traits.Folder) { - for k, v := range a.mapVal { - if !f.FoldEntry(k, v) { +func (m *nativeMap[K, V]) ConvertToNative(typeDesc reflect.Type) (any, error) { + if typeDesc == reflect.TypeFor[map[K]V]() { + return m.mapVal, nil + } + return convertMapToNative(m, m.mapVal, typeDesc) +} + +func (m *nativeMap[K, V]) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case MapType: + return m + case TypeType: + return MapType + } + return NewErr("type conversion error from '%s' to '%s'", MapType, typeVal) +} + +func (m *nativeMap[K, V]) Equal(other ref.Val) ref.Val { + otherMap, ok := other.(traits.Mapper) + if !ok { + return False + } + if m.Size() != otherMap.Size() { + return False + } + if otherNative, ok := other.(*nativeMap[K, V]); ok { + for k, v := range m.mapVal { + if otherV, found := otherNative.mapVal[k]; found { + if Equal(m.valToRefVal(v), otherNative.valToRefVal(otherV)) == False { + return False + } + continue + } + otherVal, found := otherNative.Find(m.keyToRefVal(k)) + if !found || Equal(m.valToRefVal(v), otherVal) == False { + return False + } + } + return True + } + for k, v := range m.mapVal { + otherVal, found := otherMap.Find(m.keyToRefVal(k)) + if !found { + return False + } + if Equal(m.valToRefVal(v), otherVal) == False { + return False + } + } + return True +} + +func (m *nativeMap[K, V]) Get(key ref.Val) ref.Val { + v, found := m.Find(key) + if !found { + return ValOrErr(v, "no such key: %v", key) + } + return v +} + +func (m *nativeMap[K, V]) IsZeroValue() bool { + return len(m.mapVal) == 0 +} + +func (m *nativeMap[K, V]) Fold(f traits.Folder) { + if kf, ok := f.(interface{ FoldKeyOnly() bool }); ok && kf.FoldKeyOnly() { + for k := range m.mapVal { + if !f.FoldEntry(m.keyToFoldAny(k), nil) { + break + } + } + return + } + for k, v := range m.mapVal { + if !f.FoldEntry(m.keyToFoldAny(k), m.valToFoldAny(v)) { break } } } -func newStringMapAccessor(strMap map[string]string) mapAccessor { - return &stringMapAccessor{mapVal: strMap} +type nativeMapKeyIterator[K comparable, V any] struct { + *baseIterator + m *nativeMap[K, V] + keys []K + cursor int } -type stringMapAccessor struct { - mapVal map[string]string -} - -// Find uses native map accesses to find the key, returning (value, true) if present. -// -// If the key is not found the function returns (nil, false). -func (a *stringMapAccessor) Find(key ref.Val) (ref.Val, bool) { - strKey, ok := key.(String) - if !ok { - return nil, false +func (it *nativeMapKeyIterator[K, V]) HasNext() ref.Val { + if it.cursor < len(it.keys) { + return True } - keyVal, found := a.mapVal[string(strKey)] - if !found { - return nil, false - } - return String(keyVal), true + return False } -// Iterator creates a new traits.Iterator from the string key set of the map. -func (a *stringMapAccessor) Iterator() traits.Iterator { - // Copy the keys to make their order stable. - mapKeys := make([]string, len(a.mapVal)) +func (it *nativeMapKeyIterator[K, V]) Next() ref.Val { + if it.cursor < len(it.keys) { + k := it.keys[it.cursor] + it.cursor++ + return it.m.keyToRefVal(k) + } + return nil +} + +func (m *nativeMap[K, V]) Iterator() traits.Iterator { + keys := make([]K, 0, len(m.mapVal)) + for k := range m.mapVal { + keys = append(keys, k) + } + return &nativeMapKeyIterator[K, V]{ + m: m, + keys: keys, + } +} + +func (m *nativeMap[K, V]) Size() ref.Val { + return Int(len(m.mapVal)) +} + +func (m *nativeMap[K, V]) AggregateSize(sizer AggregateSizer) uint32 { + if sz := atomic.LoadUint32(&m.aggSize); sz != 0 { + return sz + } + var total uint32 + if t, ok := getMapElementsAggregateSize(sizer, m.mapVal); ok { + total = t + } + if total == 0 { + f := foldableAggregateSizer{sizer: sizer, total: 1} + m.Fold(&f) + total = f.total + } + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&m.aggSize, total) + } + return total +} + +func (m *nativeMap[K, V]) String() string { + var sb strings.Builder + sb.WriteString("{") i := 0 - for k := range a.mapVal { - mapKeys[i] = k + for k, v := range m.mapVal { + fmt.Fprintf(&sb, "%v: %v", m.keyToRefVal(k), m.valToRefVal(v)) + if i != len(m.mapVal)-1 { + sb.WriteString(", ") + } i++ } - return &stringKeyIterator{ - mapKeys: mapKeys, - len: len(mapKeys), - } + sb.WriteString("}") + return sb.String() } -// Fold calls the FoldEntry method for each (key, value) pair in the map. -func (a *stringMapAccessor) Fold(f traits.Folder) { - for k, v := range a.mapVal { - if !f.FoldEntry(k, v) { - break - } - } +func (m *nativeMap[K, V]) format(sb *strings.Builder) { + formatMap(m, sb) } -func newStringIfaceMapAccessor(adapter Adapter, mapVal map[string]any) mapAccessor { - return &stringIfaceMapAccessor{ - Adapter: adapter, - mapVal: mapVal, - } +func (m *nativeMap[K, V]) Type() ref.Type { + return MapType } -type stringIfaceMapAccessor struct { - Adapter - mapVal map[string]any -} - -// Find uses native map accesses to find the key, returning (value, true) if present. -// -// If the key is not found the function returns (nil, false). -func (a *stringIfaceMapAccessor) Find(key ref.Val) (ref.Val, bool) { - strKey, ok := key.(String) - if !ok { - return nil, false - } - keyVal, found := a.mapVal[string(strKey)] - if !found { - return nil, false - } - return a.NativeToValue(keyVal), true -} - -// Iterator creates a new traits.Iterator from the string key set of the map. -func (a *stringIfaceMapAccessor) Iterator() traits.Iterator { - // Copy the keys to make their order stable. - mapKeys := make([]string, len(a.mapVal)) - i := 0 - for k := range a.mapVal { - mapKeys[i] = k - i++ - } - return &stringKeyIterator{ - mapKeys: mapKeys, - len: len(mapKeys), - } -} - -// Fold calls the FoldEntry method for each (key, value) pair in the map. -func (a *stringIfaceMapAccessor) Fold(f traits.Folder) { - for k, v := range a.mapVal { - if !f.FoldEntry(k, v) { - break - } - } +func (m *nativeMap[K, V]) Value() any { + return m.mapVal } // protoMap is a specialized, separate implementation of the traits.Mapper interfaces tailored to // accessing protoreflect.Map values. type protoMap struct { Adapter - value *pb.Map + value *pb.Map + aggSize uint32 } // Contains returns whether the map contains the given key. @@ -909,6 +1331,26 @@ func (m *protoMap) Size() ref.Val { return Int(m.value.Len()) } +// AggregateSize implements the AggregateSizeVisitor interface method. +func (m *protoMap) AggregateSize(sizer AggregateSizer) uint32 { + if m.value == nil { + return 1 + } + if sz := atomic.LoadUint32(&m.aggSize); sz != 0 { + return sz + } + total := uint32(1) + m.value.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + total = safeAddUint32(total, sizer.AggregateSize(k)) + total = safeAddUint32(total, sizer.AggregateSize(v)) + return true + }) + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&m.aggSize, total) + } + return total +} + // Type implements the ref.Val interface method. func (m *protoMap) Type() ref.Type { return MapType @@ -1006,6 +1448,14 @@ type interopFoldableMap struct { func (m interopFoldableMap) Fold(f traits.Folder) { it := m.Iterator() + if kf, ok := f.(interface{ FoldKeyOnly() bool }); ok && kf.FoldKeyOnly() { + for it.HasNext() == True { + if !f.FoldEntry(it.Next(), nil) { + break + } + } + return + } for it.HasNext() == True { k := it.Next() if !f.FoldEntry(k, m.Get(k)) { @@ -1068,3 +1518,11 @@ func isDelim(r rune) bool { func isLower(r rune) bool { return r >= 'a' && r <= 'z' } + +// heapCopy returns a pointer to a fresh heap copy of v. Taking &v directly would move the parameter +// to the heap on every call of the enclosing generic function, including calls that never box v. +func heapCopy[V any](v V) *V { + p := new(V) + *p = v + return p +} diff --git a/vendor/cel.dev/cel-go/common/types/memory_tracker.go b/vendor/cel.dev/cel-go/common/types/memory_tracker.go new file mode 100644 index 00000000000..ffa9fc15de8 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/memory_tracker.go @@ -0,0 +1,181 @@ +// 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 types + +import ( + "cel.dev/cel-go/common/types/ref" +) + +const ( + defaultMemoryTrackerSampleInterval = 1 +) + +// MemoryTrackerOption configures a MemoryTracker instance. +type MemoryTrackerOption func(*MemoryTracker) + +// MemoryTrackerLimit sets a limit on the peak aggregate memory observed during tracking. +// +// The tracker does not enforce the limit itself; callers should consult ExceedsLimit after +// tracking observations and terminate evaluation as appropriate. +func MemoryTrackerLimit(limit uint32) MemoryTrackerOption { + return func(t *MemoryTracker) { + t.limit = limit + t.hasLimit = true + } +} + +// MemoryTrackerSampleInterval configures how frequently Sample observations compute a size. +// +// An interval of N means every Nth call to Sample performs a size computation; intervening +// calls are skipped. Values less than 1 are treated as 1, meaning every sample is computed. +// Sampling bounds the tracking overhead for high-frequency observation points such as +// comprehension loops and bind initializers. +func MemoryTrackerSampleInterval(interval uint32) MemoryTrackerOption { + return func(t *MemoryTracker) { + if interval < 1 { + interval = 1 + } + t.sampleInterval = interval + } +} + +// MemoryTrackerSizeCalculator overrides the SizeCalculator used to compute value sizes. +func MemoryTrackerSizeCalculator(calc *SizeCalculator) MemoryTrackerOption { + return func(t *MemoryTracker) { + t.calc = calc + } +} + +// MemoryTracker records the peak aggregate memory observed during an evaluation. +// +// Memory is measured in aggregate element counts as computed by a SizeCalculator, with all +// arithmetic saturating at math.MaxUint32. The tracker is independent of any interpreter +// implementation; evaluators feed it observations at the points where values materialize +// during evaluation, such as resolved attributes, call results, constructed aggregates, and +// values built up within comprehensions or bind initializers. +// +// The peak is the largest single observation, where one Track call observes a value +// as a watermark. +// +// A MemoryTracker is stateful and intended for use by a single evaluation at a time; it is +// not safe for concurrent use. +type MemoryTracker struct { + version int + calc *SizeCalculator + limit uint32 + hasLimit bool + sampleInterval uint32 + + sampleCounts map[int64]uint32 + peak uint32 + calcLimitExceeded bool +} + +// NewMemoryTracker returns a new MemoryTracker configured with optional MemoryTrackerOption +// settings, using a default SizeCalculator when one is not provided. +func NewMemoryTracker(opts ...MemoryTrackerOption) *MemoryTracker { + t := &MemoryTracker{ + version: 0, + sampleInterval: defaultMemoryTrackerSampleInterval, + } + for _, opt := range opts { + opt(t) + } + if t.calc == nil { + t.calc = NewSizeCalculator() + } + return t +} + +// Version returns the tracking version. +func (t *MemoryTracker) Version() int { + return t.version +} + +// Track observes a value as a watermark, returning its aggregate size with saturation at math.MaxUint32. +func (t *MemoryTracker) Track(val ref.Val) uint32 { + if val == nil { + return 0 + } + switch v := val.(type) { + case Bool, Int, Uint, Double, Duration, Timestamp, Null, *Type, *Err, *Unknown: + if 1 > t.peak { + t.peak = 1 + } + return 1 + case String: + sz := t.calc.stringSize(len(v)) + if sz > t.peak { + t.peak = sz + } + return sz + case Bytes: + sz := t.calc.stringSize(len(v)) + if sz > t.peak { + t.peak = sz + } + return sz + } + + est := t.calc.ApproximateAggregateSize(val) + if est.LimitExceeded { + t.calcLimitExceeded = true + } + if est.Size > t.peak { + t.peak = est.Size + } + return est.Size +} + +// Sample observes a value for a specific node ID subject to the tracker's sample interval, +// returning the value's aggregate size when computed, or zero when the observation is skipped. +// +// The first observation of any node ID is always tracked; subsequent observations are sampled +// at multiples of the sample interval. +// +// Sample is intended for high-frequency observation points, such as accumulator values built +// up by comprehension loops or bind initializers, where sizing every iteration would be +// prohibitively expensive. +func (t *MemoryTracker) Sample(id int64, val ref.Val) uint32 { + if t.sampleCounts == nil { + t.sampleCounts = make(map[int64]uint32) + } + t.sampleCounts[id]++ + count := t.sampleCounts[id] + if count == 1 || t.sampleInterval <= 1 || count%t.sampleInterval == 0 { + return t.Track(val) + } + return 0 +} + +// Peak returns the largest single watermark observed, saturating at math.MaxUint32. +func (t *MemoryTracker) Peak() uint32 { + return t.peak +} + +// ExceedsLimit indicates whether the peak observed memory exceeds the configured limit. +// When no limit is configured, ExceedsLimit always returns false. +func (t *MemoryTracker) ExceedsLimit() bool { + return t.hasLimit && t.peak > t.limit +} + +// CalculationLimitExceeded indicates whether any tracked value was too expensive to size, +// causing the size computation to abort at the SizeCalculator's depth or traversal limits. +// +// Such observations saturate to math.MaxUint32; this signal distinguishes values which were +// too costly to measure from values whose measured size genuinely saturated uint32. +func (t *MemoryTracker) CalculationLimitExceeded() bool { + return t.calcLimitExceeded +} diff --git a/vendor/cel.dev/cel-go/common/types/native.go b/vendor/cel.dev/cel-go/common/types/native.go new file mode 100644 index 00000000000..7457d0306a5 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/native.go @@ -0,0 +1,1667 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the me. +// limitations under the License. + +package types + +import ( + "errors" + "fmt" + "reflect" + "strings" + "sync/atomic" + "time" + "unsafe" + + "google.golang.org/protobuf/reflect/protoreflect" + + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + + structpb "google.golang.org/protobuf/types/known/structpb" +) + +var ( + nativeObjTraitMask = traits.FieldTesterType | traits.IndexerType + jsonValueType = reflect.TypeFor[*structpb.Value]() + jsonStructType = reflect.TypeFor[*structpb.Struct]() + + pbMsgInterfaceType = reflect.TypeFor[protoreflect.ProtoMessage]() + refValType = reflect.TypeFor[ref.Val]() + timestampType = reflect.TypeFor[time.Time]() + durationType = reflect.TypeFor[time.Duration]() + + errDuplicatedFieldName = errors.New("field name already exists in struct") +) + +// NewNativeType constructs a NativeType instance for a Go struct reflect.Type. +func NewNativeType(rawType reflect.Type, opts ...NativeTypeOption) (*NativeType, error) { + tpOptions := NativeTypeOptions{} + for _, opt := range opts { + if err := opt(&tpOptions); err != nil { + return nil, err + } + } + return newNativeType(rawType, tpOptions) +} + +// NativeTypesFieldNameHandler is a handler for mapping a reflect.StructField to a CEL field name. +// This can be used to override the default Go struct field to CEL field name mapping. +type NativeTypesFieldNameHandler = func(field reflect.StructField) string + +// NativeTypeOptions holds options for native types. +type NativeTypeOptions struct { + fieldNameHandler NativeTypesFieldNameHandler + typeName string + adapter Adapter +} + +// NativeTypeOption is a functional option for configuring handling of native types. +type NativeTypeOption func(*NativeTypeOptions) error + +// NativeTypeAdapter sets the TypeAdapter to use when adapting nested dynamic fields for a NativeType. +func NativeTypeAdapter(adapter Adapter) NativeTypeOption { + return func(opts *NativeTypeOptions) error { + opts.adapter = adapter + return nil + } +} + +// NativeTypeAlias configures a custom CEL type name (alias) for a native type. +func NativeTypeAlias(alias string) NativeTypeOption { + return func(opts *NativeTypeOptions) error { + opts.typeName = alias + return nil + } +} + +// NativeTypeDesc describes a native Go struct type for registration with CEL. +type NativeTypeDesc struct { + refType reflect.Type + options []NativeTypeOption +} + +// ReflectType returns the reflect.Type wrapped by this descriptor. +func (d *NativeTypeDesc) ReflectType() reflect.Type { + return d.refType +} + +// Options returns the NativeTypeOption slice for this descriptor. +func (d *NativeTypeDesc) Options() []NativeTypeOption { + return d.options +} + +// NativeTypeFor constructs a NativeTypeDesc for a Go struct type T with the given options. +func NativeTypeFor[T any](opts ...NativeTypeOption) *NativeTypeDesc { + return &NativeTypeDesc{ + refType: reflect.TypeFor[T](), + options: opts, + } +} + +// ParseStructTags configures if native types field names should be overridable by CEL struct tags. +// This is equivalent to ParseStructTag("cel"). +// +// A tag starting with "-" (e.g. `cel:"-"` or `cel:"-,"`) marks the field as skipped. +// A literal "-" can be specified by single-quoting the name (e.g. `cel:"'-'"`). +// +// Skipping an embedded struct also skips the fields promoted from it, so they cannot be +// reached through the enclosing struct. +// +// The configured tag only governs what is visible to CEL expressions. Serialization, whether +// via `ConvertToNative` to a protobuf `Struct`/`Value` or via the `json.encode` and +// `yaml.encode` extension functions, is governed by the `json` tag. A field tagged +// `cel:"-"` is therefore still serialized unless it is also tagged `json:"-"`, and a field +// tagged `json:"-"` is still visible to CEL unless it is also tagged `cel:"-"`. +// Configuring ParseStructTag("json") makes CEL visibility and serialization agree. +func ParseStructTags(enabled bool) NativeTypeOption { + if enabled { + return ParseStructTag("cel") + } + return ParseStructField(nil) +} + +// ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field. +// +// A tag starting with "-" (e.g. `cel:"-"` or `cel:"-,"`) marks the field as skipped. +// A literal "-" can be specified by single-quoting the name (e.g. `cel:"'-'"`). +// +// Skipping an embedded struct also skips the fields promoted from it, so they cannot be +// reached through the enclosing struct. +// +// The configured tag only governs what is visible to CEL expressions; serialization is always +// governed by the `json` tag. See ParseStructTags for details. +func ParseStructTag(tag string) NativeTypeOption { + return ParseStructField(fieldNameByTag(tag)) +} + +// ParseStructField configures how to parse Go struct fields. It can be used to customize struct field parsing. +func ParseStructField(handler NativeTypesFieldNameHandler) NativeTypeOption { + return func(opts *NativeTypeOptions) error { + opts.fieldNameHandler = handler + return nil + } +} + +func fieldNameByTag(structTagToParse string) func(field reflect.StructField) string { + return func(field reflect.StructField) string { + tagInfo := parseStructTag(field, structTagToParse, field.Name) + if tagInfo.Skip { + return "-" + } + return tagInfo.Name + } +} + +func isSkippedFieldName(name string) bool { + return name == "" || name == "-" +} + +type nativeJSONField struct { + index []int + jsonName string + omitEmpty bool + hasTag bool +} + +// NativeType represents a CEL struct type descriptor generated from a native Go struct. +type NativeType struct { + typeName string + refType reflect.Type + fieldsByName map[string]reflect.StructField + jsonFields []nativeJSONField + adapter Adapter +} + +// Clone creates a copy of the NativeType with optional configuration overrides. +func (t *NativeType) Clone(opts ...NativeTypeOption) (*NativeType, error) { + if t == nil { + return nil, nil + } + if len(opts) == 0 { + cpy := *t + return &cpy, nil + } + tpOptions := NativeTypeOptions{ + typeName: t.typeName, + adapter: t.adapter, + } + for _, opt := range opts { + if err := opt(&tpOptions); err != nil { + return nil, err + } + } + if tpOptions.fieldNameHandler == nil { + return &NativeType{ + typeName: tpOptions.typeName, + refType: t.refType, + fieldsByName: t.fieldsByName, + jsonFields: t.jsonFields, + adapter: tpOptions.adapter, + }, nil + } + return newNativeType(t.refType, tpOptions) +} + +// ReflectType implements StructTypeDescriptor. +func (t *NativeType) ReflectType() reflect.Type { + return t.refType +} + +// Adapt implements StructTypeDescriptor. +func (t *NativeType) Adapt(adapter Adapter, value any) ref.Val { + if value == nil { + return NullValue + } + refVal := reflect.ValueOf(value) + var structPtr unsafe.Pointer + if refVal.Kind() == reflect.Ptr { + if refVal.IsNil() { + return NullValue + } + structPtr = unsafe.Pointer(refVal.Pointer()) + refVal = refVal.Elem() + } else if refVal.Kind() == reflect.Struct { + if !isDirectIface(t.refType) { + structPtr = anyDataPtr(value) + } else { + ptr := reflect.New(t.refType) + ptr.Elem().Set(refVal) + structPtr = unsafe.Pointer(ptr.Pointer()) + } + } + return &nativeObj{ + adapter: adapter, + val: value, + valType: t, + refValue: refVal, + structPtr: structPtr, + } +} + +// ConvertToNative implements ref.Val.ConvertToNative. +func (t *NativeType) ConvertToNative(typeDesc reflect.Type) (any, error) { + return nil, fmt.Errorf("type conversion error for type to '%v'", typeDesc) +} + +// ConvertToType implements ref.Val.ConvertToType. +func (t *NativeType) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case TypeType: + return TypeType + } + return NewErr("type conversion error from '%s' to '%s'", TypeType, typeVal) +} + +// Equal returns true if both type names are equal to each other. +func (t *NativeType) Equal(other ref.Val) ref.Val { + otherType, ok := other.(ref.Type) + return Bool(ok && t.TypeName() == otherType.TypeName()) +} + +// HasTrait implements the ref.Type interface method. +func (t *NativeType) HasTrait(trait int) bool { + return nativeObjTraitMask&trait == trait +} + +// String implements the fmt.Stringer interface method. +func (t *NativeType) String() string { + return t.typeName +} + +// Type implements the ref.Val interface method. +func (t *NativeType) Type() ref.Type { + return TypeType +} + +// TypeName implements the ref.Type interface method. +func (t *NativeType) TypeName() string { + return t.typeName +} + +// Value implements the ref.Val interface method. +func (t *NativeType) Value() any { + return t.typeName +} + +// NativeToValue implements the ref.TypeAdapter interface method. +func (t *NativeType) NativeToValue(value any) ref.Val { + return t.adapterFrom(nil).NativeToValue(value) +} + +func (t *NativeType) hasField(fieldName string) (reflect.StructField, bool) { + f, found := t.fieldsByName[fieldName] + if !found { + return reflect.StructField{}, false + } + return f, true +} + +// FieldNames provides the list of field names for this type. +func (t *NativeType) FieldNames() []string { + fields := make([]string, 0, len(t.fieldsByName)) + for fieldName := range t.fieldsByName { + fields = append(fields, fieldName) + } + return fields +} + +// FindFieldType looks up a field by name and provides the type and accessors. +func (t *NativeType) FindFieldType(fieldName string) (*FieldType, bool) { + refField, found := t.hasField(fieldName) + if !found { + return nil, false + } + celType, ok := convertToCelType(refField.Type) + if !ok { + return nil, false + } + return &FieldType{ + Type: celType, + IsSet: t.makeFieldTester(refField), + GetFrom: t.makeFieldGetter(refField), + }, true +} + +// NewValue constructs a new native Go struct instance populated with given field values. +func (t *NativeType) NewValue(adapter Adapter, fields map[string]ref.Val) ref.Val { + refPtr := reflect.New(t.refType) + refVal := refPtr.Elem() + for fieldName, val := range fields { + refFieldDef, isDefined := t.hasField(fieldName) + if !isDefined { + return NewErr("no such field: %s", fieldName) + } + fieldVal, err := val.ConvertToNative(refFieldDef.Type) + if err != nil { + return NewErrFromString(err.Error()) + } + refField := safeSetFieldByIndex(refVal, refFieldDef.Index) + if !refField.IsValid() { + return NewErr("cannot set field: %s", fieldName) + } + refField.Set(reflect.ValueOf(fieldVal)) + } + return adapter.NativeToValue(refPtr.Interface()) +} + +type nativeObj struct { + adapter Adapter + val any + valType *NativeType + refValue reflect.Value + structPtr unsafe.Pointer + aggSize uint32 +} + +func (o *nativeObj) ConvertToNative(typeDesc reflect.Type) (any, error) { + if o.refValue.Type() == typeDesc { + if reflect.TypeOf(o.val) == typeDesc { + return o.val, nil + } + return o.refValue.Interface(), nil + } + if typeDesc.Kind() == reflect.Pointer && o.refValue.Type() == typeDesc.Elem() { + if reflect.TypeOf(o.val) == typeDesc { + return o.val, nil + } + ptr := reflect.New(o.refValue.Type()) + ptr.Elem().Set(o.refValue) + return ptr.Interface(), nil + } + switch typeDesc { + case jsonValueType: + jsonStruct, err := o.ConvertToNative(jsonStructType) + if err != nil { + return nil, err + } + return structpb.NewStructValue(jsonStruct.(*structpb.Struct)), nil + case jsonStructType: + refVal := reflect.Indirect(o.refValue) + fields := make(map[string]*structpb.Value, len(o.valType.jsonFields)) + for _, jf := range o.valType.jsonFields { + fieldValue := safeGetFieldByIndex(refVal, jf.index) + if !fieldValue.IsValid() { + continue + } + if fieldValue.IsZero() { + if !jf.hasTag || jf.omitEmpty || fieldValue.Kind() == reflect.Pointer || fieldValue.Kind() == reflect.Slice || fieldValue.Kind() == reflect.Map || fieldValue.Kind() == reflect.Interface { + continue + } + } + fieldCELVal := o.NativeToValue(fieldValue.Interface()) + fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType) + if err != nil { + return nil, err + } + fields[jf.jsonName] = fieldJSONVal.(*structpb.Value) + } + return &structpb.Struct{Fields: fields}, nil + } + return nil, fmt.Errorf("type conversion error from '%v' to '%v'", o.Type(), typeDesc) +} + +// NativeToValue implements the ref.TypeAdapter interface method. +func (o *nativeObj) NativeToValue(value any) ref.Val { + return o.adapterFrom().NativeToValue(value) +} + +type structTagInfo struct { + Name string + OmitEmpty bool + Skip bool + HasTag bool +} + +// parseStructTag parses a struct field's tag for the given tagName (e.g. "cel" or "json"). +// +// If the tag name or leading comma-separated segment is "-" (such as `json:"-"` or `json:"-,"`), +// the field is marked as skipped. To specify a literal "-" as the field name, single quotes +// can be used, such as `json:"'-'"`. +func parseStructTag(field reflect.StructField, tagName, defaultName string) structTagInfo { + tag, found := field.Tag.Lookup(tagName) + if !found { + return structTagInfo{Name: defaultName} + } + parts := strings.Split(tag, ",") + if parts[0] == "-" { + return structTagInfo{Skip: true, HasTag: true} + } + name := strings.Trim(parts[0], "'") + if name == "" { + name = defaultName + } + omitEmpty := false + for _, opt := range parts[1:] { + if opt == "omitempty" { + omitEmpty = true + } + } + return structTagInfo{ + Name: name, + OmitEmpty: omitEmpty, + HasTag: true, + } +} + +func (o *nativeObj) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case TypeType: + return o.valType + default: + if typeVal.TypeName() == o.valType.typeName { + return o + } + } + return NewErr("type conversion error from '%s' to '%s'", o.Type(), typeVal) +} + +func (o *nativeObj) Equal(other ref.Val) ref.Val { + otherNtv, ok := other.(*nativeObj) + if !ok { + return False + } + val := o.val + otherVal := otherNtv.val + if reflect.TypeOf(val).Kind() != reflect.TypeOf(otherVal).Kind() { + val = o.refValue.Interface() + otherVal = otherNtv.refValue.Interface() + } + return Bool(reflect.DeepEqual(val, otherVal)) +} + +func (o *nativeObj) IsZeroValue() bool { + return o.refValue.IsZero() +} + +func (o *nativeObj) IsSet(field ref.Val) ref.Val { + refField, refErr := o.getReflectedField(field) + if refErr != nil { + return refErr + } + return Bool(refField.IsValid() && !refField.IsZero()) +} + +func (o *nativeObj) Get(field ref.Val) ref.Val { + refField, refErr := o.getReflectedField(field) + if refErr != nil { + return refErr + } + return adaptFieldValue(o.adapterFrom(), refField) +} + +func (o *nativeObj) adapterFrom() Adapter { + if o.adapter != nil { + return o.adapter + } + return o.valType.adapterFrom(nil) +} + +func (o *nativeObj) getReflectedField(field ref.Val) (reflect.Value, ref.Val) { + fieldName, ok := field.(String) + if !ok { + return reflect.Value{}, MaybeNoSuchOverloadErr(field) + } + fieldNameStr := string(fieldName) + refField, isDefined := o.valType.hasField(fieldNameStr) + if !isDefined { + return reflect.Value{}, NewErr("no such field: %s", fieldName) + } + refVal := reflect.Indirect(o.refValue) + return safeGetFieldByIndex(refVal, refField.Index), nil +} + +func (o *nativeObj) Type() ref.Type { + return o.valType +} + +func (o *nativeObj) Value() any { + return o.val +} + +// AggregateSize implements the AggregateSizeVisitor interface method. +func (o *nativeObj) AggregateSize(sizer AggregateSizer) uint32 { + if sz := atomic.LoadUint32(&o.aggSize); sz != 0 { + return sz + } + refVal := reflect.Indirect(o.refValue) + if !refVal.IsValid() { + return 1 + } + total := uint32(1) + for _, fieldType := range o.valType.fieldsByName { + fieldValue := safeGetFieldByIndex(refVal, fieldType.Index) + if !fieldValue.IsValid() || fieldValue.IsZero() { + continue + } + total = safeAddUint32(total, sizer.AggregateSize(fieldValue)) + } + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&o.aggSize, total) + } + return total +} + +func getNativeTypeName(refType reflect.Type, customName string) string { + if customName != "" { + return customName + } + return fmt.Sprintf("%s.%s", simplePkgAlias(refType.PkgPath()), refType.Name()) +} + +func newNativeTypes(rawType reflect.Type, options NativeTypeOptions) ([]*NativeType, error) { + nt, err := newNativeType(rawType, options) + if err != nil { + return nil, err + } + result := []*NativeType{nt} + + unwrappedRaw := rawType + if unwrappedRaw.Kind() == reflect.Pointer { + unwrappedRaw = unwrappedRaw.Elem() + } + alreadySeen := map[string]struct{}{ + unwrappedRaw.String(): {}, + } + var iterateStructMembers func(reflect.Type) + iterateStructMembers = func(t reflect.Type) { + if t.Implements(reflect.TypeFor[ref.Val]()) { + return + } + k := t.Kind() + if k == reflect.Pointer || k == reflect.Slice || k == reflect.Array { + iterateStructMembers(t.Elem()) + return + } + if k == reflect.Map { + iterateStructMembers(t.Key()) + iterateStructMembers(t.Elem()) + return + } + if t.Kind() != reflect.Struct { + return + } + if _, seen := alreadySeen[t.String()]; seen { + return + } + alreadySeen[t.String()] = struct{}{} + // Nested member structs use fieldNameHandler only (custom typeName applies to root rawType) + nestedOpts := NativeTypeOptions{ + fieldNameHandler: options.fieldNameHandler, + adapter: options.adapter, + } + nt, ntErr := newNativeType(t, nestedOpts) + if ntErr != nil { + err = ntErr + return + } + result = append(result, nt) + + for _, field := range reflect.VisibleFields(t) { + if !field.IsExported() || !isSupportedType(field.Type) { + continue + } + iterateStructMembers(field.Type) + } + } + for _, field := range reflect.VisibleFields(unwrappedRaw) { + if !field.IsExported() || !isSupportedType(field.Type) { + continue + } + iterateStructMembers(field.Type) + } + + return result, err +} + +func toFieldName(f reflect.StructField, fieldNameHandler NativeTypesFieldNameHandler) string { + if fieldNameHandler == nil { + return f.Name + } + return fieldNameHandler(f) +} + +func newNativeType(rawType reflect.Type, options NativeTypeOptions) (*NativeType, error) { + refType := rawType + if refType.Kind() == reflect.Pointer { + refType = refType.Elem() + } + if !isValidObjectType(refType) { + return nil, fmt.Errorf("unsupported reflect.Type %v, must be reflect.Struct", rawType) + } + fieldsByName, err := celFieldsByName(refType, options.fieldNameHandler) + if err != nil { + return nil, err + } + return &NativeType{ + typeName: getNativeTypeName(refType, options.typeName), + refType: refType, + fieldsByName: fieldsByName, + jsonFields: jsonFieldsOf(refType, options.fieldNameHandler), + adapter: options.adapter, + }, nil +} + +// celFieldsByName determines the fields of a struct which are visible to CEL authors, keyed by +// their CEL field name. +// +// A field is visible unless it is skipped according to the configured field name handler, which +// consults the configured struct tag (e.g. `cel:"-"`). +func celFieldsByName(refType reflect.Type, handler NativeTypesFieldNameHandler) (map[string]reflect.StructField, error) { + skipped := func(f reflect.StructField) bool { + return isSkippedFieldName(toFieldName(f, handler)) + } + fieldsByName := make(map[string]reflect.StructField) + for _, field := range visibleFields(refType, skipped) { + fieldName := toFieldName(field, handler) + if _, found := fieldsByName[fieldName]; found { + return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName) + } + fieldsByName[fieldName] = field + } + return fieldsByName, nil +} + +// jsonFieldsOf determines the fields of a struct which are included when serializing to JSON +// (and by extension YAML), along with their serialized names. +// +// A field is serialized unless it is skipped according to the `json` struct tag (`json:"-"`). +// Embedded structs follow encoding/json conventions: those without a json name are serialized +// inline via their promoted fields, those with a json name are serialized as a nested object. +// The serialized name is the json tag name when present, otherwise the CEL field name. +func jsonFieldsOf(refType reflect.Type, handler NativeTypesFieldNameHandler) []nativeJSONField { + skipped := func(f reflect.StructField) bool { + return parseStructTag(f, "json", "").Skip + } + var jsonFields []nativeJSONField + for _, field := range visibleFields(refType, skipped) { + if field.Anonymous && !hasJSONName(field) { + continue + } + if isPromotedThroughNamedJSONField(refType, field) { + continue + } + defaultName := toFieldName(field, handler) + if isSkippedFieldName(defaultName) { + defaultName = field.Name + } + tagInfo := parseStructTag(field, "json", defaultName) + jsonFields = append(jsonFields, nativeJSONField{ + index: field.Index, + jsonName: tagInfo.Name, + omitEmpty: tagInfo.OmitEmpty, + hasTag: tagInfo.HasTag, + }) + } + return jsonFields +} + +// visibleFields returns the exported, supported fields of a struct which are not skipped. +// +// A field is skipped when the predicate holds for the field itself or for any embedded struct +// through which the field is promoted, so that a skipped embedded struct cannot be reached via +// its members. +func visibleFields(refType reflect.Type, skipped func(reflect.StructField) bool) []reflect.StructField { + var fields []reflect.StructField + for _, field := range reflect.VisibleFields(refType) { + if !field.IsExported() || !isSupportedType(field.Type) { + continue + } + if isSkippedField(refType, field, skipped) { + continue + } + fields = append(fields, field) + } + return fields +} + +// isSkippedField reports whether the field, or any embedded struct through which it is +// promoted, satisfies the skip predicate. +func isSkippedField(refType reflect.Type, field reflect.StructField, skipped func(reflect.StructField) bool) bool { + if skipped(field) { + return true + } + for _, parent := range embeddedAncestors(refType, field) { + if skipped(parent) { + return true + } + } + return false +} + +// embeddedAncestors returns the chain of embedded struct fields through which a promoted +// field is reached, from outermost to innermost. The result is empty for direct fields. +func embeddedAncestors(refType reflect.Type, field reflect.StructField) []reflect.StructField { + if len(field.Index) <= 1 { + return nil + } + ancestors := make([]reflect.StructField, 0, len(field.Index)-1) + cur := refType + for _, idx := range field.Index[:len(field.Index)-1] { + if cur.Kind() == reflect.Pointer { + cur = cur.Elem() + } + if cur.Kind() != reflect.Struct || idx >= cur.NumField() { + return ancestors + } + parent := cur.Field(idx) + ancestors = append(ancestors, parent) + cur = parent.Type + } + return ancestors +} + +// hasJSONName reports whether the field carries a json tag with an explicit name. +func hasJSONName(field reflect.StructField) bool { + tagInfo := parseStructTag(field, "json", "") + return tagInfo.HasTag && tagInfo.Name != "" +} + +// isPromotedThroughNamedJSONField reports whether a promoted field is reached through an +// embedded struct with a json name, in which case the field is serialized within that +// struct's nested object rather than inline. +func isPromotedThroughNamedJSONField(refType reflect.Type, field reflect.StructField) bool { + for _, parent := range embeddedAncestors(refType, field) { + if hasJSONName(parent) { + return true + } + } + return false +} + +func adaptFieldValue(adapter Adapter, refField reflect.Value) ref.Val { + return adapter.NativeToValue(getFieldValue(adapter, refField)) +} + +func safeSetFieldByIndex(v reflect.Value, index []int) reflect.Value { + for _, i := range index { + if v.Kind() == reflect.Pointer { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + if v.Kind() != reflect.Struct || i >= v.NumField() { + return reflect.Value{} + } + v = v.Field(i) + } + return v +} + +func safeGetFieldByIndex(v reflect.Value, index []int) reflect.Value { + if v.Kind() == reflect.Pointer { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + if len(index) == 1 { + if v.Kind() != reflect.Struct || index[0] >= v.NumField() { + return reflect.Value{} + } + return v.Field(index[0]) + } + for _, i := range index { + if v.Kind() == reflect.Pointer { + if v.IsNil() { + v = reflect.Zero(v.Type().Elem()) + } else { + v = v.Elem() + } + } + if v.Kind() != reflect.Struct || i >= v.NumField() { + return reflect.Value{} + } + v = v.Field(i) + } + return v +} + +func structFieldOffset(rootType reflect.Type, index []int) (uintptr, bool) { + var offset uintptr + cur := rootType + for i, idx := range index { + if cur.Kind() != reflect.Struct || idx >= cur.NumField() { + return 0, false + } + sf := cur.Field(idx) + offset += sf.Offset + if i < len(index)-1 { + cur = sf.Type + } + } + return offset, true +} + +func (t *NativeType) expectedTypePtrs() (ptrTyp, structTyp unsafe.Pointer) { + ptrTyp = runtimeTypePtr(reflect.PointerTo(t.refType)) + if !isDirectIface(t.refType) { + structTyp = runtimeTypePtr(t.refType) + } + return ptrTyp, structTyp +} + +func (t *NativeType) structPtrFrom(obj any, expectedPtrTyp, expectedStructTyp unsafe.Pointer) (unsafe.Pointer, bool) { + e := unpackAny(obj) + if e.typ == expectedPtrTyp { + return e.ptr, e.ptr != nil + } + if expectedStructTyp != nil && e.typ == expectedStructTyp { + return e.ptr, e.ptr != nil + } + if no, ok := obj.(*nativeObj); ok && no.valType == t { + return no.structPtr, no.structPtr != nil + } + return nil, false +} + +func unwrapStruct(obj any) reflect.Value { + if no, ok := obj.(*nativeObj); ok { + return no.refValue + } + if rv, ok := obj.(ref.Val); ok { + obj = rv.Value() + } + return reflect.ValueOf(obj) +} + +func fieldTesterFor[T comparable](t *NativeType, refField reflect.StructField, offset uintptr, expectedPtrTyp, expectedStructTyp unsafe.Pointer) func(obj any) bool { + var zero T + return func(obj any) bool { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return *(*T)(unsafe.Add(ptr, offset)) != zero + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return refFieldVal.IsValid() && !refFieldVal.IsZero() + } +} + +func intFieldGetter[T ~int | ~int32 | ~int64](t *NativeType, refField reflect.StructField, offset uintptr, expectedPtrTyp, expectedStructTyp unsafe.Pointer) func(obj any) (any, error) { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return Int(*(*T)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return Int(refFieldVal.Int()), nil + } +} + +func uintFieldGetter[T ~uint | ~uint32 | ~uint64](t *NativeType, refField reflect.StructField, offset uintptr, expectedPtrTyp, expectedStructTyp unsafe.Pointer) func(obj any) (any, error) { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return Uint(*(*T)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return Uint(refFieldVal.Uint()), nil + } +} + +func floatFieldGetter[T ~float32 | ~float64](t *NativeType, refField reflect.StructField, offset uintptr, expectedPtrTyp, expectedStructTyp unsafe.Pointer) func(obj any) (any, error) { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return Double(*(*T)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return Double(refFieldVal.Float()), nil + } +} + +func (t *NativeType) adapterFrom(obj any) Adapter { + if no, ok := obj.(*nativeObj); ok && no.adapter != nil { + return no.adapter + } + if t != nil && t.adapter != nil { + return t.adapter + } + return DefaultTypeAdapter +} + +func sliceFieldGetter[T any](t *NativeType, refField reflect.StructField, offset uintptr, expectedPtrTyp, expectedStructTyp unsafe.Pointer) func(obj any) (any, error) { + return func(obj any) (any, error) { + adapter := t.adapterFrom(obj) + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return NewList(adapter, *(*[]T)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return NewList(adapter, refFieldVal.Interface().([]T)), nil + } +} + +func mapFieldGetter[K comparable, V any](t *NativeType, refField reflect.StructField, offset uintptr, expectedPtrTyp, expectedStructTyp unsafe.Pointer) func(obj any) (any, error) { + return func(obj any) (any, error) { + adapter := t.adapterFrom(obj) + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return NewMap(adapter, *(*map[K]V)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return NewMap(adapter, refFieldVal.Interface().(map[K]V)), nil + } +} + +func (t *NativeType) makeFieldTester(refField reflect.StructField) func(obj any) bool { + if offset, ok := structFieldOffset(t.refType, refField.Index); ok { + expectedPtrTyp, expectedStructTyp := t.expectedTypePtrs() + switch refField.Type.Kind() { + case reflect.Bool: + return fieldTesterFor[bool](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Int: + return fieldTesterFor[int](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Int32: + return fieldTesterFor[int32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Int64: + return fieldTesterFor[int64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Uint: + return fieldTesterFor[uint](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Uint32, reflect.Float32: + return fieldTesterFor[uint32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Uint64, reflect.Float64: + return fieldTesterFor[uint64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.String: + return fieldTesterFor[string](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.UnsafePointer, reflect.Interface: + return fieldTesterFor[unsafe.Pointer](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Struct: + if refField.Type == timestampType { + return func(obj any) bool { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return !(*time.Time)(unsafe.Add(ptr, offset)).IsZero() + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return refFieldVal.IsValid() && !refFieldVal.IsZero() + } + } + return func(obj any) bool { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return !reflect.NewAt(refField.Type, unsafe.Add(ptr, offset)).Elem().IsZero() + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return refFieldVal.IsValid() && !refFieldVal.IsZero() + } + } + } + return func(obj any) bool { + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return refFieldVal.IsValid() && !refFieldVal.IsZero() + } +} + +func (t *NativeType) makeFieldGetter(refField reflect.StructField) func(obj any) (any, error) { + if offset, ok := structFieldOffset(t.refType, refField.Index); ok { + expectedPtrTyp, expectedStructTyp := t.expectedTypePtrs() + switch refField.Type.Kind() { + case reflect.Bool: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + if *(*bool)(unsafe.Add(ptr, offset)) { + return True, nil + } + return False, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + if refFieldVal.Bool() { + return True, nil + } + return False, nil + } + case reflect.Int, reflect.Int32, reflect.Int64: + if refField.Type == durationType { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return Duration{Duration: *(*time.Duration)(unsafe.Add(ptr, offset))}, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return Duration{Duration: time.Duration(refFieldVal.Int())}, nil + } + } + switch refField.Type.Kind() { + case reflect.Int: + return intFieldGetter[int](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Int32: + return intFieldGetter[int32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Int64: + return intFieldGetter[int64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + } + case reflect.Uint, reflect.Uint32, reflect.Uint64: + switch refField.Type.Kind() { + case reflect.Uint: + return uintFieldGetter[uint](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Uint32: + return uintFieldGetter[uint32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Uint64: + return uintFieldGetter[uint64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + } + case reflect.Float32: + return floatFieldGetter[float32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.Float64: + return floatFieldGetter[float64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.String: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return String(*(*string)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return String(refFieldVal.String()), nil + } + case reflect.Slice: + if refField.Type.Elem() == reflect.TypeOf(byte(0)) { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return Bytes(*(*[]byte)(unsafe.Add(ptr, offset))), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return Bytes(refFieldVal.Bytes()), nil + } + } + switch refField.Type { + case reflect.TypeFor[[]string](): + return sliceFieldGetter[string](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]ref.Val](): + return sliceFieldGetter[ref.Val](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]int](): + return sliceFieldGetter[int](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]int64](): + return sliceFieldGetter[int64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]float64](): + return sliceFieldGetter[float64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]bool](): + return sliceFieldGetter[bool](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]int32](): + return sliceFieldGetter[int32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]uint](): + return sliceFieldGetter[uint](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]uint64](): + return sliceFieldGetter[uint64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]uint32](): + return sliceFieldGetter[uint32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]float32](): + return sliceFieldGetter[float32](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]time.Time](): + return sliceFieldGetter[time.Time](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]time.Duration](): + return sliceFieldGetter[time.Duration](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[[]any](): + return sliceFieldGetter[any](t, refField, offset, expectedPtrTyp, expectedStructTyp) + default: + if meta := getDynamicSliceMeta(refField.Type); meta.supported { + if meta.isPtrElem { + return func(obj any) (any, error) { + adapter := t.adapterFrom(obj) + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elems := *(*[]unsafe.Pointer)(unsafe.Add(ptr, offset)) + return &sliceList[unsafe.Pointer]{ + Adapter: adapter, + elems: elems, + elemTypePtr: meta.elemTypePtr, + meta: meta, + qualifyRawVal: meta.qualifyRawVal, + isNilSlice: elems == nil, + }, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return NewDynamicList(adapter, refFieldVal.Interface()), nil + } + } + return func(obj any) (any, error) { + adapter := t.adapterFrom(obj) + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + hdr := *(*unsafeSlice)(unsafe.Add(ptr, offset)) + var bytes []byte + if hdr.Data != nil && hdr.Len > 0 { + bytes = unsafe.Slice((*byte)(hdr.Data), uintptr(hdr.Len)*meta.elemStride) + } + return &sliceList[byte]{ + Adapter: adapter, + elems: bytes, + elemTypePtr: meta.elemTypePtr, + meta: meta, + qualifyRawVal: meta.qualifyRawVal, + isNilSlice: hdr.Data == nil, + }, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + if !refFieldVal.IsValid() { + return nil, nil + } + return NewDynamicList(adapter, refFieldVal.Interface()), nil + } + } + } + case reflect.Map: + switch refField.Type { + case reflect.TypeFor[map[string]string](): + return mapFieldGetter[string, string](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[string]any](): + return mapFieldGetter[string, any](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[ref.Val]ref.Val](): + return mapFieldGetter[ref.Val, ref.Val](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[int64]bool](): + return mapFieldGetter[int64, bool](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[string]int64](): + return mapFieldGetter[string, int64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[string]int](): + return mapFieldGetter[string, int](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[string]bool](): + return mapFieldGetter[string, bool](t, refField, offset, expectedPtrTyp, expectedStructTyp) + case reflect.TypeFor[map[string]float64](): + return mapFieldGetter[string, float64](t, refField, offset, expectedPtrTyp, expectedStructTyp) + } + case reflect.Struct: + if refField.Type == timestampType { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + tm := *(*time.Time)(unsafe.Add(ptr, offset)) + if tm.IsZero() { + return Timestamp{Time: time.Unix(0, 0)}, nil + } + return Timestamp{Time: tm}, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + if !refField.Type.Implements(refValType) && !refField.Type.Implements(pbMsgInterfaceType) { + expectedFieldPtrTyp := runtimeTypePtr(reflect.PointerTo(refField.Type)) + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return packAny(expectedFieldPtrTyp, unsafe.Add(ptr, offset)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + case reflect.Pointer: + elemType := refField.Type.Elem() + switch elemType.Kind() { + case reflect.Bool: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil || !*(*bool)(elemPtr) { + return False, nil + } + return True, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.Int, reflect.Int32, reflect.Int64: + if elemType == durationType { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Duration{Duration: 0}, nil + } + return Duration{Duration: *(*time.Duration)(elemPtr)}, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + switch elemType.Kind() { + case reflect.Int: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return IntZero, nil + } + return Int(*(*int)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.Int32: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return IntZero, nil + } + return Int(*(*int32)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.Int64: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return IntZero, nil + } + return Int(*(*int64)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + case reflect.Uint, reflect.Uint32, reflect.Uint64: + switch elemType.Kind() { + case reflect.Uint: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Uint(0), nil + } + return Uint(*(*uint)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.Uint32: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Uint(0), nil + } + return Uint(*(*uint32)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.Uint64: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Uint(0), nil + } + return Uint(*(*uint64)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + case reflect.Float32, reflect.Float64: + if elemType.Kind() == reflect.Float32 { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Double(0), nil + } + return Double(*(*float32)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Double(0), nil + } + return Double(*(*float64)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.String: + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return String(""), nil + } + return String(*(*string)(elemPtr)), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + case reflect.Struct: + if elemType == timestampType { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return Timestamp{Time: time.Unix(0, 0)}, nil + } + tm := *(*time.Time)(elemPtr) + if tm.IsZero() { + return Timestamp{Time: time.Unix(0, 0)}, nil + } + return Timestamp{Time: tm}, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + if !refField.Type.Implements(refValType) { + expectedFieldPtrTyp := runtimeTypePtr(refField.Type) + var zeroFieldPtr any + if isNullZeroPointerType(refField.Type) { + zeroFieldPtr = NullValue + } else { + zeroFieldPtr = reflect.New(elemType).Interface() + } + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + elemPtr := *(*unsafe.Pointer)(unsafe.Add(ptr, offset)) + if elemPtr == nil { + return zeroFieldPtr, nil + } + return packAny(expectedFieldPtrTyp, elemPtr), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + } + case reflect.Interface: + if refField.Type == reflect.TypeFor[any]() { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + val := *(*any)(unsafe.Add(ptr, offset)) + return t.adapterFrom(obj).NativeToValue(val), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + if refField.Type.Implements(refValType) { + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + val := *(*ref.Val)(unsafe.Add(ptr, offset)) + if val == nil { + return NullValue, nil + } + return val, nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + return func(obj any) (any, error) { + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + val := reflect.NewAt(refField.Type, unsafe.Add(ptr, offset)).Elem().Interface() + return t.adapterFrom(obj).NativeToValue(val), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(t.adapterFrom(obj), refFieldVal), nil + } + } + return func(obj any) (any, error) { + adapter := t.adapterFrom(obj) + if ptr, ok := t.structPtrFrom(obj, expectedPtrTyp, expectedStructTyp); ok { + return getFieldValue(adapter, reflect.NewAt(refField.Type, unsafe.Add(ptr, offset)).Elem()), nil + } + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(adapter, refFieldVal), nil + } + } + return func(obj any) (any, error) { + adapter := t.adapterFrom(obj) + refVal := unwrapStruct(obj) + refFieldVal := safeGetFieldByIndex(refVal, refField.Index) + return getFieldValue(adapter, refFieldVal), nil + } +} + +func isNullZeroPointerType(t reflect.Type) bool { + switch t { + case boolWrapperType, byteWrapperType, doubleWrapperType, floatWrapperType, + int32WrapperType, int64WrapperType, stringWrapperType, uint32WrapperType, + uint64WrapperType, jsonValueType, anyValueType: + return true + default: + return false + } +} + +func getFieldValue(adapter Adapter, refField reflect.Value) any { + if adapter == nil { + adapter = DefaultTypeAdapter + } + if !refField.IsValid() { + return nil + } + switch refField.Kind() { + case reflect.Bool: + if refField.Bool() { + return True + } + return False + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if refField.Type() == durationType { + return Duration{Duration: time.Duration(refField.Int())} + } + return Int(refField.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return Uint(refField.Uint()) + case reflect.Float32, reflect.Float64: + return Double(refField.Float()) + case reflect.String: + return String(refField.String()) + case reflect.Slice: + if refField.Type().Elem() == reflect.TypeOf(byte(0)) { + return Bytes(refField.Bytes()) + } + if refField.Type() == reflect.TypeFor[[]string]() { + return NewStringList(adapter, refField.Interface().([]string)) + } + if refField.Type() == reflect.TypeFor[[]ref.Val]() { + return NewRefValList(adapter, refField.Interface().([]ref.Val)) + } + case reflect.Map: + if refField.Type() == reflect.TypeFor[map[string]string]() { + return NewStringStringMap(adapter, refField.Interface().(map[string]string)) + } + if refField.Type() == reflect.TypeFor[map[string]any]() { + return NewStringInterfaceMap(adapter, refField.Interface().(map[string]any)) + } + if refField.Type() == reflect.TypeFor[map[ref.Val]ref.Val]() { + return NewRefValMap(adapter, refField.Interface().(map[ref.Val]ref.Val)) + } + case reflect.Struct: + if refField.Type() == timestampType { + if refField.IsZero() { + return Timestamp{Time: time.Unix(0, 0)} + } + return Timestamp{Time: refField.Interface().(time.Time)} + } + case reflect.Pointer: + if refField.IsZero() { + if isNullZeroPointerType(refField.Type()) { + return NullValue + } + elemType := refField.Type().Elem() + switch elemType.Kind() { + case reflect.Bool: + return False + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if elemType == durationType { + return Duration{Duration: 0} + } + return IntZero + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return Uint(0) + case reflect.Float32, reflect.Float64: + return Double(0) + case reflect.String: + return String("") + case reflect.Struct: + if elemType == timestampType { + return Timestamp{Time: time.Unix(0, 0)} + } + } + return reflect.New(elemType).Interface() + } + elemType := refField.Type().Elem() + switch elemType.Kind() { + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, reflect.String: + return getFieldValue(adapter, refField.Elem()) + case reflect.Struct: + if elemType == timestampType { + return getFieldValue(adapter, refField.Elem()) + } + } + case reflect.Interface: + if refField.IsNil() { + return NullValue + } + return adapter.NativeToValue(refField.Interface()) + } + return refField.Interface() +} + +func simplePkgAlias(pkgPath string) string { + paths := strings.Split(pkgPath, "/") + return paths[len(paths)-1] +} + +func isValidObjectType(refType reflect.Type) bool { + return refType.Kind() == reflect.Struct +} + +func isSupportedType(refType reflect.Type) bool { + switch refType.Kind() { + case reflect.Chan, reflect.Complex64, reflect.Complex128, reflect.Func, reflect.UnsafePointer, reflect.Uintptr: + return false + case reflect.Array, reflect.Slice: + return isSupportedType(refType.Elem()) + case reflect.Map: + return isSupportedType(refType.Key()) && isSupportedType(refType.Elem()) + } + return true +} + +func convertToCelType(refType reflect.Type) (*Type, bool) { + switch refType.Kind() { + case reflect.Bool: + return BoolType, true + case reflect.Float32, reflect.Float64: + return DoubleType, true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if refType == durationType { + return DurationType, true + } + return IntType, true + case reflect.String: + return StringType, true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return UintType, true + case reflect.Array, reflect.Slice: + refElem := refType.Elem() + if refElem.Kind() == reflect.Uint8 { + return BytesType, true + } + elemType, ok := convertToCelType(refElem) + if !ok { + return nil, false + } + return NewListType(elemType), true + case reflect.Map: + keyType, ok := convertToCelType(refType.Key()) + if !ok { + return nil, false + } + elemType, ok := convertToCelType(refType.Elem()) + if !ok { + return nil, false + } + return NewMapType(keyType, elemType), true + case reflect.Struct: + if refType == timestampType { + return TimestampType, true + } + if refType.Implements(refValType) { + emptyCelVal := reflect.New(refType).Elem().Interface().(ref.Val) + return emptyCelVal.Type().(*Type), true + } + return NewObjectType(getNativeTypeName(refType, "")), true + case reflect.Pointer: + if refType.Implements(refValType) { + emptyCelVal := reflect.New(refType.Elem()).Interface().(ref.Val) + return emptyCelVal.Type().(*Type), true + } + if refType.Implements(pbMsgInterfaceType) { + pbMsg := reflect.New(refType.Elem()).Interface().(protoreflect.ProtoMessage) + return NewObjectType(string(pbMsg.ProtoReflect().Descriptor().FullName())), true + } + return convertToCelType(refType.Elem()) + case reflect.Interface: + return DynType, true + } + return nil, false +} diff --git a/vendor/github.com/google/cel-go/common/types/null.go b/vendor/cel.dev/cel-go/common/types/null.go similarity index 98% rename from vendor/github.com/google/cel-go/common/types/null.go rename to vendor/cel.dev/cel-go/common/types/null.go index 671e1ee5c02..990609c695e 100644 --- a/vendor/github.com/google/cel-go/common/types/null.go +++ b/vendor/cel.dev/cel-go/common/types/null.go @@ -21,7 +21,7 @@ import ( "google.golang.org/protobuf/proto" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" diff --git a/vendor/github.com/google/cel-go/common/types/object.go b/vendor/cel.dev/cel-go/common/types/object.go similarity index 89% rename from vendor/github.com/google/cel-go/common/types/object.go rename to vendor/cel.dev/cel-go/common/types/object.go index bb2a09e879f..82bc3e330d1 100644 --- a/vendor/github.com/google/cel-go/common/types/object.go +++ b/vendor/cel.dev/cel-go/common/types/object.go @@ -19,13 +19,14 @@ import ( "reflect" "sort" "strings" + "sync/atomic" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/google/cel-go/common/types/pb" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/pb" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" @@ -36,6 +37,7 @@ type protoObj struct { value proto.Message typeDesc *pb.TypeDescription typeValue ref.Val + aggSize uint32 } // NewObject returns an object based on a proto.Message value which handles @@ -167,9 +169,23 @@ func (o *protoObj) Value() any { return o.value } -type protoObjField struct { - fd protoreflect.FieldDescriptor - v protoreflect.Value +// AggregateSize implements the AggregateSizeVisitor interface method. +func (o *protoObj) AggregateSize(sizer AggregateSizer) uint32 { + if o.value == nil { + return 1 + } + if sz := atomic.LoadUint32(&o.aggSize); sz != 0 { + return sz + } + total := uint32(1) + o.value.ProtoReflect().Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + total = safeAddUint32(total, sizer.AggregateSize(v)) + return true + }) + if cacheableAggregateSize(sizer) { + atomic.StoreUint32(&o.aggSize, total) + } + return total } func (o *protoObj) format(sb *strings.Builder) { diff --git a/vendor/github.com/google/cel-go/common/types/optional.go b/vendor/cel.dev/cel-go/common/types/optional.go similarity index 88% rename from vendor/github.com/google/cel-go/common/types/optional.go rename to vendor/cel.dev/cel-go/common/types/optional.go index 0d861823d27..71e9b5cab9e 100644 --- a/vendor/github.com/google/cel-go/common/types/optional.go +++ b/vendor/cel.dev/cel-go/common/types/optional.go @@ -20,7 +20,7 @@ import ( "reflect" "strings" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) var ( @@ -120,3 +120,14 @@ func (o *Optional) Value() any { } return o.value.Value() } + +// AggregateSize implements the AggregateSizeVisitor interface method. +// +// An empty optional has size 1, as every value does, so that the aggregate size of a container +// is at least 1 plus the number of entries it holds. +func (o *Optional) AggregateSize(sizer AggregateSizer) uint32 { + if !o.HasValue() { + return 1 + } + return safeAddUint32(1, sizer.AggregateSize(o.value)) +} diff --git a/vendor/github.com/google/cel-go/common/types/overflow.go b/vendor/cel.dev/cel-go/common/types/overflow.go similarity index 97% rename from vendor/github.com/google/cel-go/common/types/overflow.go rename to vendor/cel.dev/cel-go/common/types/overflow.go index dcb66ef5961..49b15377fca 100644 --- a/vendor/github.com/google/cel-go/common/types/overflow.go +++ b/vendor/cel.dev/cel-go/common/types/overflow.go @@ -427,3 +427,24 @@ func uint64ToInt64Lossless(v uint64) (int64, bool) { i, err := uint64ToInt64Checked(v) return i, err == nil } + +func safeAddUint32(a, b uint32) uint32 { + if math.MaxUint32-a < b { + return math.MaxUint32 + } + return a + b +} + +func safeUint32FromInt(n int) uint32 { + if n < 0 || uint64(n) > math.MaxUint32 { + return math.MaxUint32 + } + return uint32(n) +} + +func safeUint32FromBoxedInt(v Int) uint32 { + if v < 0 || v > math.MaxUint32 { + return math.MaxUint32 + } + return uint32(v) +} diff --git a/vendor/github.com/google/cel-go/common/types/pb/BUILD.bazel b/vendor/cel.dev/cel-go/common/types/pb/BUILD.bazel similarity index 96% rename from vendor/github.com/google/cel-go/common/types/pb/BUILD.bazel rename to vendor/cel.dev/cel-go/common/types/pb/BUILD.bazel index e2b9d37b569..66d333c368a 100644 --- a/vendor/github.com/google/cel-go/common/types/pb/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/types/pb/BUILD.bazel @@ -15,7 +15,7 @@ go_library( "pb.go", "type.go", ], - importpath = "github.com/google/cel-go/common/types/pb", + importpath = "cel.dev/cel-go/common/types/pb", deps = [ "@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library", "@org_golang_google_protobuf//encoding/protowire:go_default_library", diff --git a/vendor/github.com/google/cel-go/common/types/pb/checked.go b/vendor/cel.dev/cel-go/common/types/pb/checked.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/pb/checked.go rename to vendor/cel.dev/cel-go/common/types/pb/checked.go diff --git a/vendor/github.com/google/cel-go/common/types/pb/enum.go b/vendor/cel.dev/cel-go/common/types/pb/enum.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/pb/enum.go rename to vendor/cel.dev/cel-go/common/types/pb/enum.go diff --git a/vendor/github.com/google/cel-go/common/types/pb/equal.go b/vendor/cel.dev/cel-go/common/types/pb/equal.go similarity index 95% rename from vendor/github.com/google/cel-go/common/types/pb/equal.go rename to vendor/cel.dev/cel-go/common/types/pb/equal.go index 76893d85eab..275a2a61c97 100644 --- a/vendor/github.com/google/cel-go/common/types/pb/equal.go +++ b/vendor/cel.dev/cel-go/common/types/pb/equal.go @@ -29,10 +29,10 @@ import ( // // - Messages must share the same instance of the type descriptor // - Known set fields are compared using semantics equality -// - Bytes are compared using bytes.Equal -// - Scalar values are compared with operator == -// - List and map types are equal if they have the same length and all elements are equal -// - Messages are equal if they share the same descriptor and all set fields are equal +// - Bytes are compared using bytes.Equal +// - Scalar values are compared with operator == +// - List and map types are equal if they have the same length and all elements are equal +// - Messages are equal if they share the same descriptor and all set fields are equal // - Unknown fields are compared using byte equality // - NaN values are not equal to each other // - google.protobuf.Any values are unpacked before comparison diff --git a/vendor/github.com/google/cel-go/common/types/pb/file.go b/vendor/cel.dev/cel-go/common/types/pb/file.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/pb/file.go rename to vendor/cel.dev/cel-go/common/types/pb/file.go diff --git a/vendor/github.com/google/cel-go/common/types/pb/pb.go b/vendor/cel.dev/cel-go/common/types/pb/pb.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/pb/pb.go rename to vendor/cel.dev/cel-go/common/types/pb/pb.go diff --git a/vendor/github.com/google/cel-go/common/types/pb/type.go b/vendor/cel.dev/cel-go/common/types/pb/type.go similarity index 93% rename from vendor/github.com/google/cel-go/common/types/pb/type.go rename to vendor/cel.dev/cel-go/common/types/pb/type.go index 8d7d1b299f5..92c2a2d3627 100644 --- a/vendor/github.com/google/cel-go/common/types/pb/type.go +++ b/vendor/cel.dev/cel-go/common/types/pb/type.go @@ -258,21 +258,31 @@ func (fd *FieldDescription) Documentation() string { // This function implements the FieldType.IsSet function contract which can be used to operate on // more than just protobuf field accesses; however, the target here must be a protobuf.Message. func (fd *FieldDescription) IsSet(target any) bool { + var pbRef protoreflect.Message switch v := target.(type) { case proto.Message: - pbRef := v.ProtoReflect() - pbDesc := pbRef.Descriptor() - if pbDesc == fd.desc.ContainingMessage() { - // When the target protobuf shares the same message descriptor instance as the field - // descriptor, use the cached field descriptor value. - return pbRef.Has(fd.desc) - } - // Otherwise, fallback to a dynamic lookup of the field descriptor from the target - // instance as an attempt to use the cached field descriptor will result in a panic. - return pbRef.Has(pbDesc.Fields().ByName(protoreflect.Name(fd.Name()))) + pbRef = v.ProtoReflect() + case protoreflect.Message: + pbRef = v default: return false } + if pbRef == nil || !pbRef.IsValid() { + return false + } + pbDesc := pbRef.Descriptor() + if pbDesc == fd.desc.ContainingMessage() || (fd.desc.IsExtension() && pbDesc.FullName() == fd.desc.ContainingMessage().FullName()) { + // When the target protobuf shares the same message descriptor instance as the field + // descriptor, use the cached field descriptor value. + return pbRef.Has(fd.desc) + } + // Otherwise, fallback to a dynamic lookup of the field descriptor from the target + // instance as an attempt to use the cached field descriptor will result in a panic. + f := pbDesc.Fields().ByName(protoreflect.Name(fd.Name())) + if f == nil { + return false + } + return pbRef.Has(f) } // GetFrom returns the accessor method associated with the field on the proto generated struct. @@ -282,22 +292,33 @@ func (fd *FieldDescription) IsSet(target any) bool { // This function implements the FieldType.GetFrom function contract which can be used to operate // on more than just protobuf field accesses; however, the target here must be a protobuf.Message. func (fd *FieldDescription) GetFrom(target any) (any, error) { - v, ok := target.(proto.Message) - if !ok { + var pbRef protoreflect.Message + switch v := target.(type) { + case proto.Message: + pbRef = v.ProtoReflect() + case protoreflect.Message: + pbRef = v + default: + return nil, fmt.Errorf("unsupported field selection target: (%T)%v", target, target) + } + if pbRef == nil { return nil, fmt.Errorf("unsupported field selection target: (%T)%v", target, target) } - pbRef := v.ProtoReflect() pbDesc := pbRef.Descriptor() - var fieldVal any - if pbDesc == fd.desc.ContainingMessage() { + var f protoreflect.FieldDescriptor + if pbDesc == fd.desc.ContainingMessage() || (fd.desc.IsExtension() && pbDesc.FullName() == fd.desc.ContainingMessage().FullName()) { // When the target protobuf shares the same message descriptor instance as the field // descriptor, use the cached field descriptor value. - fieldVal = pbRef.Get(fd.desc).Interface() + f = fd.desc } else { // Otherwise, fallback to a dynamic lookup of the field descriptor from the target // instance as an attempt to use the cached field descriptor will result in a panic. - fieldVal = pbRef.Get(pbDesc.Fields().ByName(protoreflect.Name(fd.Name()))).Interface() + f = pbDesc.Fields().ByName(protoreflect.Name(fd.Name())) + if f == nil { + return nil, fmt.Errorf("unsupported field selection target: (%T)%v", target, target) + } } + fieldVal := pbRef.Get(f).Interface() switch fv := fieldVal.(type) { // Fast-path return for primitive types. case bool, []byte, float32, float64, int32, int64, string, uint32, uint64, protoreflect.List: diff --git a/vendor/cel.dev/cel-go/common/types/provider.go b/vendor/cel.dev/cel-go/common/types/provider.go new file mode 100644 index 00000000000..d901dc662ef --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/provider.go @@ -0,0 +1,1283 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "encoding/json" + "fmt" + "maps" + "reflect" + "sync" + "sync/atomic" + "time" + "unsafe" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "cel.dev/cel-go/common/types/pb" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + anypb "google.golang.org/protobuf/types/known/anypb" + dpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + tpb "google.golang.org/protobuf/types/known/timestamppb" +) + +// Adapter converts native Go values of varying type and complexity to equivalent CEL values. +type Adapter = ref.TypeAdapter + +// Provider specifies functions for creating new object instances and for resolving +// enum values by name. +type Provider interface { + // EnumValue returns the numeric value of the given enum value name. + EnumValue(enumName string) ref.Val + + // FindIdent takes a qualified identifier name and returns a ref.Val if one exists. + FindIdent(identName string) (ref.Val, bool) + + // FindStructType returns the Type give a qualified type name. + // + // For historical reasons, only struct types are expected to be returned through this + // method, and the type values are expected to be wrapped in a TypeType instance using + // TypeTypeWithParam(). + // + // Returns false if not found. + FindStructType(structType string) (*Type, bool) + + // FindStructFieldNames returns the field names associated with the type, if the type + // is found. + FindStructFieldNames(structType string) ([]string, bool) + + // FindStructFieldType returns the field type for a checked type value. Returns + // false if the field could not be found. + FindStructFieldType(structType, fieldName string) (*FieldType, bool) + + // NewValue creates a new type value from a qualified name and map of field + // name to value. + // + // Note, for each value, the Val.ConvertToNative function will be invoked + // to convert the Val to the field's native type. If an error occurs during + // conversion, the NewValue will be a types.Err. + NewValue(structType string, fields map[string]ref.Val) ref.Val +} + +// FieldType represents a field's type value and whether that field supports presence detection. +type FieldType struct { + // Type of the field as a CEL native type value. + Type *Type + + // IsSet indicates whether the field is set on an input object. + IsSet ref.FieldTester + + // GetFrom retrieves the field value on the input object, if set. + GetFrom ref.FieldGetter + + // IsJSONField + IsJSONField bool +} + +// Registry provides type information for a set of registered types. +type Registry struct { + revTypeMap map[string]*Type + structTypes map[string]StructTypeDescriptor + reflectTypes map[reflect.Type]StructTypeDescriptor + shared atomic.Bool + pbdb *pb.Db + provider Provider + adapter Adapter + nativeOptions NativeTypeOptions +} + +// NewRegistry accepts a list of proto message instances, ref.Type instances, or RegistryOption +// functions and returns a type provider. +func NewRegistry(types ...any) (*Registry, error) { + r, err := NewProtoRegistry() + if err != nil { + return nil, err + } + if err := registerTypeItems(r, types...); err != nil { + return nil, err + } + return r, nil +} + +// RegistryOption configures the behavior of the registry. +type RegistryOption func(r *Registry) (*Registry, error) + +// JSONFieldNames configures JSON field name support within the protobuf types in the registry. +func JSONFieldNames(enabled bool) RegistryOption { + return func(r *Registry) (*Registry, error) { + err := r.WithJSONFieldNames(enabled) + return r, err + } +} + +// ProtoTypeDefs creates a RegistryOption which registers the individual proto messages with the registry. +func ProtoTypeDefs(types ...proto.Message) RegistryOption { + return func(r *Registry) (*Registry, error) { + for _, msgType := range types { + err := r.RegisterMessage(msgType) + if err != nil { + return nil, err + } + } + return r, nil + } +} + +// Types creates a RegistryOption which registers individual custom type references or descriptors with the registry. +func Types(types ...ref.Type) RegistryOption { + return func(r *Registry) (*Registry, error) { + err := r.RegisterType(types...) + if err != nil { + return nil, err + } + return r, nil + } +} + +// NewProtoRegistry creates a proto-based registry with a set of configurable options. +func NewProtoRegistry(opts ...RegistryOption) (*Registry, error) { + r := NewEmptyRegistry() + err := r.RegisterType( + BoolType, + BytesType, + DoubleType, + DurationType, + IntType, + ListType, + MapType, + NullType, + StringType, + TimestampType, + TypeType, + UintType) + if err != nil { + return nil, err + } + // This block ensures that the well-known protobuf types are registered by default. + for _, fd := range r.pbdb.FileDescriptions() { + err = r.registerAllTypes(fd) + if err != nil { + return nil, err + } + } + for _, opt := range opts { + r, err = opt(r) + if err != nil { + return nil, err + } + } + return r, nil +} + +// NewEmptyRegistry returns a registry which is completely unconfigured. +func NewEmptyRegistry() *Registry { + return &Registry{ + revTypeMap: make(map[string]*Type), + structTypes: make(map[string]StructTypeDescriptor), + reflectTypes: make(map[reflect.Type]StructTypeDescriptor), + pbdb: pb.NewDb(), + } +} + +// ComposeTypes accepts a provider, adapter, and a list of types (ref.Type, proto.Message, protoreflect.FileDescriptor, or RegistryOption) +// and either: +// - Determines the provider and adapter are the same instance and a *Registry and registers the listed types via RegisterType or +// one of the other registration methods as appropriate. +// - Determines the provider and adapter are not the same, or not a *Registry and creates a new composed *Registry which references +// the new type information first and then proxies to the underlying provider and adapter methods as appropriate. +func ComposeTypes(provider Provider, adapter Adapter, types ...any) (Provider, Adapter, error) { + reg, isReg := provider.(*Registry) + aReg, isAdapterReg := adapter.(*Registry) + if isReg && isAdapterReg && reg == aReg { + if err := registerTypeItems(reg, types...); err != nil { + return nil, nil, err + } + return reg, reg, nil + } + + composedReg, err := NewRegistry(types...) + if err != nil { + return nil, nil, err + } + composedReg.provider = provider + composedReg.adapter = adapter + return composedReg, composedReg, nil +} + +// Copy copies the current state of the registry into its own memory space. +func (p *Registry) Copy() *Registry { + if p == nil { + return nil + } + p.shared.Store(true) + cpy := &Registry{ + revTypeMap: p.revTypeMap, + structTypes: p.structTypes, + reflectTypes: p.reflectTypes, + nativeOptions: p.nativeOptions, + pbdb: p.pbdb, + provider: p.provider, + adapter: p.adapter, + } + cpy.shared.Store(true) + return cpy +} + +func (p *Registry) ensureMutable() { + if p.shared.Load() { + p.revTypeMap = maps.Clone(p.revTypeMap) + p.structTypes = maps.Clone(p.structTypes) + p.reflectTypes = maps.Clone(p.reflectTypes) + for k, st := range p.structTypes { + if nt, isNative := st.(*NativeType); isNative { + cloned, _ := nt.Clone(NativeTypeAdapter(p)) + p.structTypes[k] = cloned + if rt := cloned.ReflectType(); rt != nil { + p.reflectTypes[rt] = cloned + if rt.Kind() == reflect.Ptr { + p.reflectTypes[rt.Elem()] = cloned + } else { + p.reflectTypes[reflect.PointerTo(rt)] = cloned + } + } + } + } + p.pbdb = p.pbdb.Copy() + p.shared.Store(false) + } +} + +// JSONFieldNames returns whether json field names are enabled in this registry. +func (p *Registry) JSONFieldNames() bool { + return p.pbdb.JSONFieldNames() +} + +// WithJSONFieldNames configures the registry with the JSON field name support enabled or disabled. +func (p *Registry) WithJSONFieldNames(enabled bool) error { + if enabled == p.pbdb.JSONFieldNames() { + return nil + } + p.ensureMutable() + newDB := pb.NewDb(pb.JSONFieldNames(enabled)) + files := p.pbdb.FileDescriptions() + for _, fd := range files { + _, err := newDB.RegisterDescriptor(fd.FileDescriptor()) + if err != nil { + return err + } + } + p.pbdb = newDB + return nil +} + +// EnumValue returns the numeric value of the given enum value name. +func (p *Registry) EnumValue(enumName string) ref.Val { + enumVal, found := p.pbdb.DescribeEnum(enumName) + if !found { + if p.provider != nil { + return p.provider.EnumValue(enumName) + } + return NewErr("unknown enum name '%s'", enumName) + } + return Int(enumVal.Value()) +} + +// FindFieldType returns the field type for a checked type value. Returns false if +// the field could not be found. +// +// Deprecated: use FindStructFieldType +func (p *Registry) FindFieldType(structType, fieldName string) (*ref.FieldType, bool) { + structType = sanitizeStructTypeName(structType) + if st, found := p.structTypes[structType]; found { + if ft, found := st.FindFieldType(fieldName); found { + exprType, err := TypeToExprType(ft.Type) + if err != nil { + return nil, false + } + return makeRefFieldType(exprType, ft.IsSet, ft.GetFrom, ft.IsJSONField), true + } + } + if msgType, found := p.pbdb.DescribeType(structType); found { + if field, found := msgType.FieldByName(fieldName); found { + return makeRefFieldType(field.CheckedType(), field.IsSet, field.GetFrom, p.pbdb.JSONFieldNames() && fieldName == field.JSONName()), true + } + } + if p.provider != nil { + if ft, ok := p.provider.FindStructFieldType(structType, fieldName); ok && ft != nil { + exprType, err := TypeToExprType(ft.Type) + if err != nil { + return nil, false + } + return makeRefFieldType(exprType, ft.IsSet, ft.GetFrom, ft.IsJSONField), true + } + } + return nil, false +} + +// FindStructFieldNames returns the set of field names for the given struct type, +// if the type exists in the registry. +func (p *Registry) FindStructFieldNames(structType string) ([]string, bool) { + structType = sanitizeStructTypeName(structType) + if st, found := p.structTypes[structType]; found { + return st.FieldNames(), true + } + if msgType, found := p.pbdb.DescribeType(structType); found { + fieldMap := msgType.FieldMap() + fields := make([]string, len(fieldMap)) + idx := 0 + for f := range fieldMap { + fields[idx] = f + idx++ + } + return fields, true + } + if p.provider != nil { + return p.provider.FindStructFieldNames(structType) + } + return []string{}, false +} + +// FindStructFieldType returns the field type for a checked type value. Returns +// false if the field could not be found. +func (p *Registry) FindStructFieldType(structType, fieldName string) (*FieldType, bool) { + structType = sanitizeStructTypeName(structType) + if st, found := p.structTypes[structType]; found { + if ft, found := st.FindFieldType(fieldName); found { + return ft, true + } + } + if msgType, found := p.pbdb.DescribeType(structType); found { + if field, found := msgType.FieldByName(fieldName); found { + return &FieldType{ + Type: fieldDescToCELType(field), + IsSet: field.IsSet, + GetFrom: field.GetFrom, + IsJSONField: p.pbdb.JSONFieldNames() && fieldName == field.JSONName(), + }, true + } + } + if p.provider != nil { + return p.provider.FindStructFieldType(structType, fieldName) + } + return nil, false +} + +// FindStructFieldDescription returns documentation for a field if available. +// Returns false if the field could not be found. +func (p *Registry) FindStructFieldDescription(structType, fieldName string) (string, bool) { + structType = sanitizeStructTypeName(structType) + if msgType, found := p.pbdb.DescribeType(structType); found { + if field, found := msgType.FieldByName(fieldName); found { + return field.Documentation(), true + } + } + if p.provider != nil { + if pd, ok := p.provider.(interface { + FindStructFieldDescription(string, string) (string, bool) + }); ok { + return pd.FindStructFieldDescription(structType, fieldName) + } + } + return "", false +} + +// FindIdent takes a qualified identifier name and returns a ref.Val if one exists. +func (p *Registry) FindIdent(identName string) (ref.Val, bool) { + if t, found := p.revTypeMap[identName]; found { + return t, true + } + if t, found := p.revTypeMap[sanitizeStructTypeName(identName)]; found { + return t, true + } + if enumVal, found := p.pbdb.DescribeEnum(identName); found { + return Int(enumVal.Value()), true + } + if p.provider != nil { + return p.provider.FindIdent(identName) + } + return nil, false +} + +// FindType looks up the Type given a qualified typeName. Returns false if not found. +// +// Deprecated: use FindStructType +func (p *Registry) FindType(structType string) (*exprpb.Type, bool) { + structType = sanitizeStructTypeName(structType) + if p.hasStructType(structType) { + return makeExprMessageType(structType), true + } + if p.provider != nil { + if tp, ok := p.provider.(ref.TypeProvider); ok { + return tp.FindType(structType) + } + if _, ok := p.provider.FindStructType(structType); ok { + return makeExprMessageType(structType), true + } + } + return nil, false +} + +// FindStructType returns the Type give a qualified type name. +// +// For historical reasons, only struct types are expected to be returned through this +// method, and the type values are expected to be wrapped in a TypeType instance using +// TypeTypeWithParam(). +// +// Returns false if not found. +func (p *Registry) FindStructType(structType string) (*Type, bool) { + structType = sanitizeStructTypeName(structType) + if p.hasStructType(structType) { + return NewTypeTypeWithParam(NewObjectType(structType)), true + } + if p.provider != nil { + return p.provider.FindStructType(structType) + } + return nil, false +} + +// NewValue creates a new type value from a qualified name and map of field +// name to value. +// +// Note, for each value, the Val.ConvertToNative function will be invoked +// to convert the Val to the field's native type. If an error occurs during +// conversion, the NewValue will be a types.Err. +func (p *Registry) NewValue(structType string, fields map[string]ref.Val) ref.Val { + structType = sanitizeStructTypeName(structType) + if st, found := p.structTypes[structType]; found { + return st.NewValue(p, fields) + } + td, found := p.pbdb.DescribeType(structType) + if !found { + if p.provider != nil { + return p.provider.NewValue(structType, fields) + } + return NewErr("unknown type '%s'", structType) + } + msg := td.New() + for name, value := range fields { + field, found := td.FieldByName(name) + if !found { + return NewErr("no such field: %s", name) + } + err := msgSetField(msg, field, value) + if err != nil { + return &Err{error: err} + } + } + return p.NativeToValue(msg.Interface()) +} + +// RegisterDescriptor registers the contents of a protocol buffer `FileDescriptor`. +func (p *Registry) RegisterDescriptor(fileDesc protoreflect.FileDescriptor) error { + p.ensureMutable() + fd, err := p.pbdb.RegisterDescriptor(fileDesc) + if err != nil { + return err + } + return p.registerAllTypes(fd) +} + +// RegisterMessage registers a protocol buffer message and its dependencies. +func (p *Registry) RegisterMessage(message proto.Message) error { + p.ensureMutable() + fd, err := p.pbdb.RegisterMessage(message) + if err != nil { + return err + } + return p.registerAllTypes(fd) +} + +// RegisterType registers a type value with the provider which ensures the provider is aware of how to +// map the type to an identifier. +// +// If the `ref.Type` value is a `*types.Type` it will be registered directly by its runtime type name. +// If the `ref.Type` value is not a `*types.Type` instance, a `*types.Type` instance which reflects the +// traits present on the input and the runtime type name. By default this foreign type will be treated +// as a types.StructKind. To avoid potential issues where the `ref.Type` values does not match the +// generated `*types.Type` instance, consider always using the `*types.Type` to represent type extensions +// to CEL, even when they're not based on protobuf types. +func (p *Registry) RegisterType(types ...ref.Type) error { + for _, t := range types { + typeName := sanitizeStructTypeName(t.TypeName()) + existing, found := p.revTypeMap[typeName] + celType := maybeForeignType(t) + if found { + if !existing.IsEquivalentType(celType) { + return fmt.Errorf("type registration conflict. found: %v, input: %v", existing, celType) + } + if existing.traitMask != celType.traitMask { + return fmt.Errorf( + "type registered with conflicting traits: %v with traits %v, input: %v", + existing.TypeName(), existing.traitMask, celType.traitMask) + } + continue + } + + p.ensureMutable() + p.revTypeMap[typeName] = celType + if st, ok := t.(StructTypeDescriptor); ok { + // Conflicts are gated above so if we see a struct here, it's safe to register. + if nt, isNative := t.(*NativeType); isNative { + cloned, err := nt.Clone(NativeTypeAdapter(p)) + if err != nil { + return err + } + t = cloned + st = cloned + } + p.structTypes[typeName] = st + if rt := st.ReflectType(); rt != nil { + p.reflectTypes[rt] = st + if rt.Kind() == reflect.Ptr { + p.reflectTypes[rt.Elem()] = st + } else { + p.reflectTypes[reflect.PointerTo(rt)] = st + } + } + } + } + return nil +} + +// RegisterNativeType creates nativeType instances for the given reflect.Type and registers them. +func (p *Registry) RegisterNativeType(refType reflect.Type, opts ...NativeTypeOption) error { + nativeOpts := p.nativeOptions + // Note: consider cloning NativeType instances so that the registry may be reset if the type is copied to another registry. + nativeOpts.adapter = p + for _, opt := range opts { + if err := opt(&nativeOpts); err != nil { + return err + } + } + result, err := newNativeTypes(refType, nativeOpts) + if err != nil { + return err + } + for _, nt := range result { + if err := p.RegisterType(nt); err != nil { + return err + } + } + return nil +} + +// RegisterNativeTypeDesc creates nativeType instances for a NativeTypeDesc and registers them. +func (p *Registry) RegisterNativeTypeDesc(desc *NativeTypeDesc) error { + if desc == nil { + return nil + } + return p.RegisterNativeType(desc.ReflectType(), desc.Options()...) +} + +func (p *Registry) findStructDescriptorByReflectType(rt reflect.Type) (StructTypeDescriptor, bool) { + if rt == nil { + return nil, false + } + if st, found := p.reflectTypes[rt]; found { + return st, true + } + if rt.Kind() == reflect.Ptr { + if st, found := p.reflectTypes[rt.Elem()]; found { + return st, true + } + } else { + if st, found := p.reflectTypes[reflect.PointerTo(rt)]; found { + return st, true + } + } + return nil, false +} + +// NativeToValue converts various "native" types to ref.Val with this specific implementation +// providing support for custom proto-based types. +// +// This method should be the inverse of ref.Val.ConvertToNative. +func (p *Registry) NativeToValue(value any) ref.Val { + switch v := value.(type) { + case nil: + return NullValue + case *Bool: + if v != nil { + return *v + } + case *Bytes: + if v != nil { + return *v + } + case *Double: + if v != nil { + return *v + } + case *Int: + if v != nil { + return *v + } + case *String: + if v != nil { + return *v + } + case *Uint: + if v != nil { + return *v + } + case ref.Val: + return v + case bool: + return Bool(v) + case int: + return Int(v) + case int32: + return Int(v) + case int64: + return Int(v) + case uint: + return Uint(v) + case uint32: + return Uint(v) + case uint64: + return Uint(v) + case float32: + return Double(v) + case float64: + return Double(v) + case string: + return String(v) + case *dpb.Duration: + return Duration{Duration: v.AsDuration()} + case time.Duration: + return Duration{Duration: v} + case *tpb.Timestamp: + return Timestamp{Time: v.AsTime()} + case time.Time: + return Timestamp{Time: v} + case *bool: + if v != nil { + return Bool(*v) + } + case *float32: + if v != nil { + return Double(*v) + } + case *float64: + if v != nil { + return Double(*v) + } + case *int: + if v != nil { + return Int(*v) + } + case *int32: + if v != nil { + return Int(*v) + } + case *int64: + if v != nil { + return Int(*v) + } + case *string: + if v != nil { + return String(*v) + } + case *uint: + if v != nil { + return Uint(*v) + } + case *uint32: + if v != nil { + return Uint(*v) + } + case *uint64: + if v != nil { + return Uint(*v) + } + case json.Number: + if i, err := v.Int64(); err == nil { + return Int(i) + } + if f, err := v.Float64(); err == nil { + return Double(f) + } + case json.RawMessage: + var rawVal any + if err := json.Unmarshal(v, &rawVal); err == nil { + return p.NativeToValue(rawVal) + } + case []byte: + return Bytes(v) + // specializations for common lists types. + case []string: + return NewList(p, v) + case []ref.Val: + return NewList(p, v) + case []int: + return NewList(p, v) + case []int64: + return NewList(p, v) + case []int32: + return NewList(p, v) + case []uint: + return NewList(p, v) + case []uint64: + return NewList(p, v) + case []uint32: + return NewList(p, v) + case []float64: + return NewList(p, v) + case []float32: + return NewList(p, v) + case []bool: + return NewList(p, v) + case []any: + return NewList(p, v) + // specializations for common map types. + case map[string]string: + return NewMap(p, v) + case map[string]any: + return NewMap(p, v) + case map[ref.Val]ref.Val: + return NewMap(p, v) + case map[int64]bool: + return NewMap(p, v) + case map[string]int64: + return NewMap(p, v) + case map[string]int: + return NewMap(p, v) + case map[string]bool: + return NewMap(p, v) + case map[string]float64: + return NewMap(p, v) + case map[int64]string: + return NewMap(p, v) + case map[int]string: + return NewMap(p, v) + // additional specializations may be added upon request / need. + case *anypb.Any: + if v == nil { + return UnsupportedRefValConversionErr(v) + } + unpackedAny, err := v.UnmarshalNew() + if err != nil { + return NewErr("anypb.UnmarshalNew() failed for type %q: %v", v.GetTypeUrl(), err) + } + return p.NativeToValue(unpackedAny) + case *structpb.NullValue, structpb.NullValue: + return NullValue + case *structpb.ListValue: + return NewJSONList(p, v) + case *structpb.Struct: + return NewJSONStruct(p, v) + case protoreflect.EnumNumber: + return Int(v) + case proto.Message: + if v == nil { + return UnsupportedRefValConversionErr(v) + } + typeName := string(v.ProtoReflect().Descriptor().FullName()) + pbdb := p.pbdb + if pbdb == nil { + pbdb = pb.DefaultDb + } + td, found := pbdb.DescribeType(typeName) + if !found { + if p.adapter != nil { + return p.adapter.NativeToValue(value) + } + return NewErr("unknown type: '%s'", typeName) + } + unwrapped, isUnwrapped, err := td.MaybeUnwrap(v) + if err != nil { + return UnsupportedRefValConversionErr(v) + } + if isUnwrapped { + return p.NativeToValue(unwrapped) + } + typeVal, found := p.FindIdent(typeName) + if !found { + return NewErr("unknown type: '%s'", typeName) + } + return NewObject(p, td, typeVal, v) + case *pb.Map: + return NewProtoMap(p, v) + case protoreflect.List: + return NewProtoList(p, v) + case protoreflect.Message: + if v == nil { + return UnsupportedRefValConversionErr(v) + } + return p.NativeToValue(v.Interface()) + case protoreflect.Value: + return p.NativeToValue(v.Interface()) + default: + rt := reflect.TypeOf(value) + if len(p.reflectTypes) > 0 { + if st, found := p.findStructDescriptorByReflectType(rt); found { + return st.Adapt(p, value) + } + } + refVal := reflect.ValueOf(v) + if refVal.Kind() == reflect.Ptr { + if refVal.IsNil() { + break + } + refVal = refVal.Elem() + } + switch refVal.Kind() { + case reflect.Array, reflect.Slice: + if refVal.Type().Elem() == reflect.TypeOf(byte(0)) { + if refVal.CanAddr() { + return Bytes(refVal.Bytes()) + } + tmp := reflect.New(refVal.Type()) + tmp.Elem().Set(refVal) + return Bytes(tmp.Elem().Bytes()) + } + return NewDynamicList(p, v) + case reflect.Map: + return NewDynamicMap(p, v) + case reflect.Bool: + return Bool(refVal.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return Int(refVal.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return Uint(refVal.Uint()) + case reflect.Float32, reflect.Float64: + return Double(refVal.Float()) + case reflect.String: + return String(refVal.String()) + } + } + if p.adapter != nil { + return p.adapter.NativeToValue(value) + } + return UnsupportedRefValConversionErr(value) +} + +func (p *Registry) registerAllTypes(fd *pb.FileDescription) error { + for _, typeName := range fd.GetTypeNames() { + // skip well-known type names since they're automatically sanitized + // during NewObjectType() calls. + if _, found := checkedWellKnowns[typeName]; found { + continue + } + err := p.RegisterType(NewObjectTypeValue(typeName)) + if err != nil { + return err + } + } + return nil +} + +func (p *Registry) hasStructType(structType string) bool { + if _, found := p.structTypes[structType]; found { + return true + } + _, found := p.pbdb.DescribeType(structType) + return found +} + +func sanitizeStructTypeName(structType string) string { + if len(structType) > 0 && structType[0] == '.' { + return structType[1:] + } + return structType +} + +func registerTypeItems(r *Registry, types ...any) error { + opts := make([]any, 0, len(types)) + items := make([]any, 0, len(types)) + for _, t := range types { + switch t.(type) { + case NativeTypeOption: + opts = append(opts, t) + default: + items = append(items, t) + } + } + for _, opt := range opts { + if err := registerTypeItem(r, opt); err != nil { + return err + } + } + for _, item := range items { + if err := registerTypeItem(r, item); err != nil { + return err + } + } + return nil +} + +func registerTypeItem(r *Registry, t any) error { + switch v := t.(type) { + case proto.Message: + return r.RegisterMessage(v) + case protoreflect.FileDescriptor: + return r.RegisterDescriptor(v) + case ref.Type: + return r.RegisterType(v) + case reflect.Type: + return r.RegisterNativeType(v) + case reflect.Value: + return r.RegisterNativeType(v.Type()) + case *NativeTypeDesc: + return r.RegisterNativeTypeDesc(v) + case NativeTypeOption: + return v(&r.nativeOptions) + case RegistryOption: + _, err := v(r) + return err + default: + return fmt.Errorf("unsupported type: %v (%T) must be reflect.Type, reflect.Value, or NativeTypeDesc", t, t) + } +} + +func makeExprMessageType(structType string) *exprpb.Type { + return &exprpb.Type{ + TypeKind: &exprpb.Type_Type{ + Type: &exprpb.Type{ + TypeKind: &exprpb.Type_MessageType{ + MessageType: structType, + }, + }, + }, + } +} + +func makeRefFieldType(t *exprpb.Type, isSet ref.FieldTester, getFrom ref.FieldGetter, isJSONField bool) *ref.FieldType { + return &ref.FieldType{ + Type: t, + IsSet: isSet, + GetFrom: getFrom, + IsJSONField: isJSONField, + } +} + +func fieldDescToCELType(field *pb.FieldDescription) *Type { + if field.IsMap() { + return NewMapType( + singularFieldDescToCELType(field.KeyType), + singularFieldDescToCELType(field.ValueType)) + } + if field.IsList() { + return NewListType(singularFieldDescToCELType(field)) + } + return singularFieldDescToCELType(field) +} + +func singularFieldDescToCELType(field *pb.FieldDescription) *Type { + if field.IsMessage() { + return NewObjectType(string(field.Descriptor().Message().FullName())) + } + if field.IsEnum() { + return IntType + } + return ProtoCELPrimitives[field.ProtoKind()] +} + +// defaultTypeAdapter converts go native types to CEL values. +type defaultTypeAdapter struct{} + +var ( + // DefaultTypeAdapter adapts canonical CEL types from their equivalent Go values. + DefaultTypeAdapter = &defaultTypeAdapter{} + emptyRegistry = &Registry{pbdb: pb.DefaultDb} +) + +// NativeToValue implements the ref.TypeAdapter interface. +func (a *defaultTypeAdapter) NativeToValue(value any) ref.Val { + return emptyRegistry.NativeToValue(value) +} + +func msgSetField(target protoreflect.Message, field *pb.FieldDescription, val ref.Val) error { + if field.IsList() { + lv := target.NewField(field.Descriptor()) + list, ok := val.(traits.Lister) + if !ok { + return unsupportedTypeConversionError(field, val) + } + err := msgSetListField(lv.List(), field, list) + if err != nil { + return err + } + target.Set(field.Descriptor(), lv) + return nil + } + if field.IsMap() { + mv := target.NewField(field.Descriptor()) + mp, ok := val.(traits.Mapper) + if !ok { + return unsupportedTypeConversionError(field, val) + } + err := msgSetMapField(mv.Map(), field, mp) + if err != nil { + return err + } + target.Set(field.Descriptor(), mv) + return nil + } + v, err := val.ConvertToNative(field.ReflectType()) + if err != nil { + return fieldTypeConversionError(field, err) + } + if v == nil { + return nil + } + switch pv := v.(type) { + case proto.Message: + v = pv.ProtoReflect() + } + target.Set(field.Descriptor(), protoreflect.ValueOf(v)) + return nil +} + +func msgSetListField(target protoreflect.List, listField *pb.FieldDescription, listVal traits.Lister) error { + elemReflectType := listField.ReflectType().Elem() + for i := Int(0); i < listVal.Size().(Int); i++ { + elem := listVal.Get(i) + elemVal, err := elem.ConvertToNative(elemReflectType) + if err != nil { + return fieldTypeConversionError(listField, err) + } + if elemVal == nil { + continue + } + switch ev := elemVal.(type) { + case proto.Message: + elemVal = ev.ProtoReflect() + } + target.Append(protoreflect.ValueOf(elemVal)) + } + return nil +} + +func msgSetMapField(target protoreflect.Map, mapField *pb.FieldDescription, mapVal traits.Mapper) error { + targetKeyType := mapField.KeyType.ReflectType() + targetValType := mapField.ValueType.ReflectType() + it := mapVal.Iterator() + for it.HasNext() == True { + key := it.Next() + val := mapVal.Get(key) + k, err := key.ConvertToNative(targetKeyType) + if err != nil { + return fieldTypeConversionError(mapField, err) + } + v, err := val.ConvertToNative(targetValType) + if err != nil { + return fieldTypeConversionError(mapField, err) + } + if v == nil { + continue + } + switch pv := v.(type) { + case proto.Message: + v = pv.ProtoReflect() + } + target.Set(protoreflect.ValueOf(k).MapKey(), protoreflect.ValueOf(v)) + } + return nil +} + +func unsupportedTypeConversionError(field *pb.FieldDescription, val ref.Val) error { + msgName := field.Descriptor().ContainingMessage().FullName() + return fmt.Errorf("unsupported field type for %v.%v: %v", msgName, field.Name(), val.Type()) +} + +func fieldTypeConversionError(field *pb.FieldDescription, err error) error { + msgName := field.Descriptor().ContainingMessage().FullName() + return fmt.Errorf("field type conversion error for %v.%v value type: %v", msgName, field.Name(), err) +} + +// emptyInterface mirrors Go runtime's internal eface representation: +// +// type eface struct { +// _type *_type +// data unsafe.Pointer +// } +// +// It is used exclusively for zero-copy qualification and direct type extraction. +type emptyInterface struct { + typ unsafe.Pointer + ptr unsafe.Pointer +} + +// packAny constructs an empty interface value from a raw runtime *_type pointer and data pointer. +func packAny(typ, ptr unsafe.Pointer) any { + var a any + e := (*emptyInterface)(unsafe.Pointer(&a)) + e.typ = typ + e.ptr = ptr + return a +} + +// unpackAny extracts the raw runtime *_type and data pointers from an empty interface value. +func unpackAny(v any) emptyInterface { + return *(*emptyInterface)(unsafe.Pointer(&v)) +} + +// anyDataPtr returns the data pointer word from an empty interface value. +func anyDataPtr(v any) unsafe.Pointer { + return (*emptyInterface)(unsafe.Pointer(&v)).ptr +} + +// runtimeTypePtr returns the Go runtime *_type pointer for the given reflect.Type. +func runtimeTypePtr(rt reflect.Type) unsafe.Pointer { + return unpackAny(reflect.Zero(rt).Interface()).typ +} + +// elemTypePtrFor returns the runtime *_type pointer for T if and only if T is a non-empty +// raw struct type suitable for zero-copy boxing via packAny(typ, unsafe.Pointer(&elem)). +// +// For indirect-interface structs, data points to the struct value and typ is T's *_type. +// For single-pointer direct-interface structs, data holds the pointer directly at offset 0, +// so &elem has the exact representation of a *T in an emptyInterface and typ is *T's *_type. +func elemTypePtrFor[T any]() unsafe.Pointer { + t := reflect.TypeFor[T]() + if t.Kind() != reflect.Struct || t.Size() == 0 || !isQualifyRawStructType(t) { + return nil + } + var z T + if e := unpackAny(z); e.ptr != nil { + return e.typ + } + var zp *T + return unpackAny(zp).typ +} + +// elemTypePtrForType is the reflect.Type equivalent of elemTypePtrFor. +func elemTypePtrForType(elemType reflect.Type) unsafe.Pointer { + if elemType == nil || elemType.Kind() != reflect.Struct || elemType.Size() == 0 || !isQualifyRawStructType(elemType) { + return nil + } + if isDirectIface(elemType) { + return runtimeTypePtr(reflect.PointerTo(elemType)) + } + return runtimeTypePtr(elemType) +} + +// isDirectIface determines whether a type's values fit directly into an interface's data word +// according to Go runtime conventions (pointers, single-pointer structs/arrays, maps, funcs, chans). +func isDirectIface(t reflect.Type) bool { + switch t.Kind() { + case reflect.Pointer, reflect.UnsafePointer, reflect.Chan, reflect.Map, reflect.Func: + return true + case reflect.Struct: + return t.NumField() == 1 && isDirectIface(t.Field(0).Type) + case reflect.Array: + return t.Len() == 1 && isDirectIface(t.Elem()) + default: + return false + } +} + +type unsafeSlice struct { + Data unsafe.Pointer + Len int + Cap int +} + +type dynamicSliceMeta struct { + sliceType reflect.Type + sliceTypePtr unsafe.Pointer + elemTypePtr unsafe.Pointer + elemStride uintptr + qualifyRawVal bool + isPtrElem bool + supported bool +} + +var dynamicSliceMetaCache sync.Map + +func isQualifyRawStruct[T any]() bool { + return isQualifyRawStructType(reflect.TypeFor[T]()) +} + +func isQualifyRawStructType(rt reflect.Type) bool { + if rt == nil || rt.Implements(reflect.TypeFor[ref.Val]()) || rt.Implements(reflect.TypeFor[protoreflect.ProtoMessage]()) { + return false + } + if rt.Kind() == reflect.Struct { + return rt != reflect.TypeFor[time.Time]() + } + if rt.Kind() == reflect.Ptr && rt.Elem().Kind() == reflect.Struct { + return rt.Elem() != reflect.TypeFor[time.Time]() + } + return false +} + +var unsupportedDynamicSliceMeta = &dynamicSliceMeta{supported: false} + +func getDynamicSliceMeta(rt reflect.Type) *dynamicSliceMeta { + if rt == nil || rt.Kind() != reflect.Slice { + return unsupportedDynamicSliceMeta + } + if v, ok := dynamicSliceMetaCache.Load(rt); ok { + return v.(*dynamicSliceMeta) + } + sliceTypPtr := runtimeTypePtr(rt) + elemType := rt.Elem() + var meta *dynamicSliceMeta + switch elemType.Kind() { + case reflect.Pointer: + meta = &dynamicSliceMeta{ + sliceType: rt, + sliceTypePtr: sliceTypPtr, + elemTypePtr: runtimeTypePtr(elemType), + qualifyRawVal: isQualifyRawStructType(elemType), + isPtrElem: true, + supported: true, + } + case reflect.Struct: + if elemTyp := elemTypePtrForType(elemType); elemTyp != nil { + meta = &dynamicSliceMeta{ + sliceType: rt, + sliceTypePtr: sliceTypPtr, + elemTypePtr: elemTyp, + elemStride: elemType.Size(), + qualifyRawVal: true, + supported: true, + } + } + } + if meta != nil && meta.supported { + dynamicSliceMetaCache.Store(rt, meta) + return meta + } + return unsupportedDynamicSliceMeta +} + +var ( + // ProtoCELPrimitives provides a map from the protoreflect Kind to the equivalent CEL type. + ProtoCELPrimitives = map[protoreflect.Kind]*Type{ + protoreflect.BoolKind: BoolType, + protoreflect.BytesKind: BytesType, + protoreflect.DoubleKind: DoubleType, + protoreflect.FloatKind: DoubleType, + protoreflect.Int32Kind: IntType, + protoreflect.Int64Kind: IntType, + protoreflect.Sint32Kind: IntType, + protoreflect.Sint64Kind: IntType, + protoreflect.Uint32Kind: UintType, + protoreflect.Uint64Kind: UintType, + protoreflect.Fixed32Kind: UintType, + protoreflect.Fixed64Kind: UintType, + protoreflect.Sfixed32Kind: IntType, + protoreflect.Sfixed64Kind: IntType, + protoreflect.StringKind: StringType, + } +) diff --git a/vendor/github.com/google/cel-go/common/types/ref/BUILD.bazel b/vendor/cel.dev/cel-go/common/types/ref/BUILD.bazel similarity index 89% rename from vendor/github.com/google/cel-go/common/types/ref/BUILD.bazel rename to vendor/cel.dev/cel-go/common/types/ref/BUILD.bazel index 79330c33216..9df5779d8bd 100644 --- a/vendor/github.com/google/cel-go/common/types/ref/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/types/ref/BUILD.bazel @@ -11,7 +11,7 @@ go_library( "provider.go", "reference.go", ], - importpath = "github.com/google/cel-go/common/types/ref", + importpath = "cel.dev/cel-go/common/types/ref", deps = [ "@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", diff --git a/vendor/github.com/google/cel-go/common/types/ref/provider.go b/vendor/cel.dev/cel-go/common/types/ref/provider.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/ref/provider.go rename to vendor/cel.dev/cel-go/common/types/ref/provider.go diff --git a/vendor/github.com/google/cel-go/common/types/ref/reference.go b/vendor/cel.dev/cel-go/common/types/ref/reference.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/ref/reference.go rename to vendor/cel.dev/cel-go/common/types/ref/reference.go diff --git a/vendor/cel.dev/cel-go/common/types/regex.go b/vendor/cel.dev/cel-go/common/types/regex.go new file mode 100644 index 00000000000..286dd5ee4ad --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/regex.go @@ -0,0 +1,49 @@ +// 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 types + +import ( + "fmt" + "regexp" + "regexp/syntax" +) + +// RegexProgramSize calculates the instruction count (program plan size) of a regex pattern. +func RegexProgramSize(pattern string) (int, error) { + re, err := syntax.Parse(pattern, syntax.Perl) + if err != nil { + return 0, err + } + prog, err := syntax.Compile(re.Simplify()) + if err != nil { + return 0, err + } + return len(prog.Inst), nil +} + +// CompileRegexWithLimit compiles a regex pattern and verifies that its program plan size does not exceed limit. +// A limit <= 0 means unbounded. +func CompileRegexWithLimit(pattern string, limit int) (*regexp.Regexp, error) { + if limit > 0 { + sz, err := RegexProgramSize(pattern) + if err != nil { + return nil, err + } + if sz > limit { + return nil, fmt.Errorf("regex program size %d exceeds limit of %d", sz, limit) + } + } + return regexp.Compile(pattern) +} diff --git a/vendor/cel.dev/cel-go/common/types/size_calc.go b/vendor/cel.dev/cel-go/common/types/size_calc.go new file mode 100644 index 00000000000..6093c80367e --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/size_calc.go @@ -0,0 +1,600 @@ +// 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 types + +import ( + "math" + "reflect" + "sync" + "time" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + + structpb "google.golang.org/protobuf/types/known/structpb" +) + +const ( + defaultSizeCalculatorMaxDepth = 5 + defaultSizeCalculatorMaxTraversal = 10000 + defaultSizeCalculatorStringUnitLength = 10 +) + +// SizeCalculatorOption configures a SizeCalculator instance. +type SizeCalculatorOption func(*SizeCalculator) + +// SizeCalculatorMaxDepth sets the maximum object depth limit before saturating to math.MaxUint32. +func SizeCalculatorMaxDepth(depth int) SizeCalculatorOption { + return func(s *SizeCalculator) { + s.maxDepth = depth + } +} + +// SizeCalculatorMaxTraversal sets the maximum object traversal limit before saturating to math.MaxUint32. +func SizeCalculatorMaxTraversal(traversal int) SizeCalculatorOption { + return func(s *SizeCalculator) { + s.maxTraversal = traversal + } +} + +// SizeCalculatorStringUnitLength sets the number of string or bytes value bytes which count as +// a single element (default 10). Values less than 1 are treated as 1, meaning each byte counts +// as a whole element. +// +// String sizes are measured in bytes rather than characters so that sizing large values is +// O(1) rather than a full UTF-8 scan per observation; byte length is never smaller than the +// character count, so byte-based sizing is conservative for limit enforcement. +func SizeCalculatorStringUnitLength(length int) SizeCalculatorOption { + return func(s *SizeCalculator) { + if length < 1 { + length = 1 + } + s.stringUnitLength = length + } +} + +// SizeCalculator calculates the recursive element size of values. +// +// Aggregate values may memoize their computed size on first calculation as an optimization +// for repeated sizing of shared structures. The memoized size reflects the configuration of +// the calculator which first sized the value; hosts requiring differently configured +// calculators, e.g. distinct depth or traversal limits, should not share value instances +// across them. Memoized totals are also a snapshot of the value's contents at first sizing: +// hosts which mutate data underlying a sized aggregate, e.g. a proto message held as a list +// element, will observe the total computed before the mutation. Sizes computed from +// calculations aborted at the depth or traversal limits are never memoized. +type SizeCalculator struct { + version int + maxDepth int + maxTraversal int + stringUnitLength int +} + +// NewSizeCalculator returns a new SizeCalculator configured with optional SizeCalculatorOption settings. +func NewSizeCalculator(opts ...SizeCalculatorOption) *SizeCalculator { + s := &SizeCalculator{ + version: 0, + maxDepth: defaultSizeCalculatorMaxDepth, + maxTraversal: defaultSizeCalculatorMaxTraversal, + stringUnitLength: defaultSizeCalculatorStringUnitLength, + } + for _, opt := range opts { + opt(s) + } + return s +} + +// Version returns the calculation version. +func (s *SizeCalculator) Version() int { + return s.version +} + +var sizeContextPool = sync.Pool{ + New: func() any { + return &sizeContext{} + }, +} + +type sizeContext struct { + calc *SizeCalculator + depth int + traversalCount int + limitExceeded bool +} + +func (c *sizeContext) visitNode() bool { + c.traversalCount++ + if c.traversalCount > c.calc.maxTraversal || c.depth > c.calc.maxDepth { + c.limitExceeded = true + return false + } + return true +} + +// aggregateSizeStatus exposes whether the in-flight size computation has exceeded the +// calculator's depth or traversal limits. +type aggregateSizeStatus interface { + aggregateSizeLimitExceeded() bool +} + +// aggregateSizeLimitExceeded implements the aggregateSizeStatus interface method. +func (c *sizeContext) aggregateSizeLimitExceeded() bool { + return c.limitExceeded +} + +// cacheableAggregateSize reports whether a size computed with the given sizer is safe to +// memoize on the value. Only totals from computations which verifiably stayed within the +// calculator's depth and traversal limits are stable properties of the value; totals from +// aborted computations depend on where in the traversal the value was encountered and would +// poison the memoized size. +func cacheableAggregateSize(sizer AggregateSizer) bool { + status, ok := sizer.(aggregateSizeStatus) + return ok && !status.aggregateSizeLimitExceeded() +} + +// ApproximateAggregateSize captures the outcome of an aggregate size computation. +// +// The Size saturates at math.MaxUint32 when the accumulated element count overflows uint32. +// LimitExceeded reports the computation was aborted because the value was too expensive to +// traverse (too deep, or too many nodes visited); in that case Size is also math.MaxUint32, +// but the value's true size may be smaller — the two conditions are distinguishable by the flag. +type ApproximateAggregateSize struct { + Size uint32 + LimitExceeded bool +} + +// ApproximateAggregateSize returns the aggregate size of the input value along with an indication +// of whether the computation was aborted due to the calculator's depth or traversal limits. +func (s *SizeCalculator) ApproximateAggregateSize(val any) ApproximateAggregateSize { + ctx := sizeContextPool.Get().(*sizeContext) + ctx.calc = s + est := ctx.estimateAggregateSize(val) + ctx.calc = nil + + sizeContextPool.Put(ctx) + return est +} + +// AggregateSize implements the AggregateSizer interface by returning the computed size. +func (s *SizeCalculator) AggregateSize(val any) uint32 { + return s.ApproximateAggregateSize(val).Size +} + +// stringSize converts a character or byte length to an element count where stringUnitLength +// characters count as a single element, rounding up with a minimum size of 1. +func (s *SizeCalculator) stringSize(length int) uint32 { + if length <= 0 { + return 1 + } + return safeUint32FromInt((length + s.stringUnitLength - 1) / s.stringUnitLength) +} + +// AggregateSize implements the ref.Val interface and allows for the generation of nested +// child context values which are necessary for correct traversal count tracking. +func (c *sizeContext) AggregateSize(val any) uint32 { + c.depth++ + res := c.aggregateSize(val) + c.depth-- + return res +} + +func (c *sizeContext) aggregateSize(val any) uint32 { + if !c.visitNode() { + return math.MaxUint32 + } + switch v := val.(type) { + case String: + return c.calc.stringSize(len(v)) + case Bytes: + return c.calc.stringSize(len(v)) + case AggregateSizeVisitor: + return v.AggregateSize(c) + case traits.Foldable: + f := foldableAggregateSizer{sizer: c, total: 1} + v.Fold(&f) + return f.total + case traits.Mapper: + total := uint32(1) + it := v.Iterator() + for it.HasNext() == True { + key := it.Next() + val, _ := v.Find(key) + total = safeAddUint32(total, c.AggregateSize(key)) + total = safeAddUint32(total, c.AggregateSize(val)) + } + return total + case traits.Lister: + total := uint32(1) + it := v.Iterator() + for it.HasNext() == True { + total = safeAddUint32(total, c.AggregateSize(it.Next())) + } + return total + case traits.Sizer: + return max(1, safeUint32FromBoxedInt(v.Size().(Int))) + case Bool, Int, Uint, Double, Duration, Timestamp, Null, *Type, *Err, *Unknown: + return 1 + case ref.Val: + return c.aggregateSize(v.Value()) + case protoreflect.Value: + return getProtoValueAggregateSize(c, v) + case protoreflect.MapKey: + return getProtoValueAggregateSize(c, v.Value()) + case protoreflect.Message: + return getProtoMessageAggregateSize(c, v) + case protoreflect.List: + return getProtoListAggregateSize(c, v) + case protoreflect.Map: + return getProtoMapAggregateSize(c, v) + case proto.Message: + if v == nil { + return 1 + } + return getProtoMessageAggregateSize(c, v.ProtoReflect()) + case string: + return c.calc.stringSize(len(v)) + case []byte: + return c.calc.stringSize(len(v)) + case int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + float32, float64, bool, time.Time, time.Duration, nil: + return 1 + case reflect.Value: + return getReflectValueAggregateSize(c, v) + default: + return getReflectValueAggregateSize(c, reflect.ValueOf(val)) + } +} + +// estimateAggregateSize evaluates the aggregate size using this context and resets state. +func (c *sizeContext) estimateAggregateSize(val any) ApproximateAggregateSize { + c.depth = 0 + c.traversalCount = 0 + c.limitExceeded = false + + size := c.AggregateSize(val) + return ApproximateAggregateSize{Size: size, LimitExceeded: c.limitExceeded} +} + +func getProtoValueAggregateSize(c *sizeContext, v protoreflect.Value) uint32 { + if !v.IsValid() { + return 1 + } + switch val := v.Interface().(type) { + case protoreflect.Message: + return getProtoMessageAggregateSize(c, val) + case protoreflect.List: + return getProtoListAggregateSize(c, val) + case protoreflect.Map: + return getProtoMapAggregateSize(c, val) + case string: + return c.calc.stringSize(len(val)) + case []byte: + return c.calc.stringSize(len(val)) + default: + return 1 + } +} + +func getProtoFieldAggregateSize(c *sizeContext, fd protoreflect.FieldDescriptor, v protoreflect.Value) uint32 { + if !c.visitNode() { + return math.MaxUint32 + } + if fd.IsMap() { + c.depth++ + sz := getProtoMapAggregateSize(c, v.Map()) + c.depth-- + return sz + } + if fd.IsList() { + c.depth++ + sz := getProtoListAggregateSize(c, v.List()) + c.depth-- + return sz + } + if fd.Message() != nil { + return c.AggregateSize(v.Message()) + } + switch fd.Kind() { + case protoreflect.StringKind: + return c.calc.stringSize(len(v.String())) + case protoreflect.BytesKind: + return c.calc.stringSize(len(v.Bytes())) + default: + return 1 + } +} + +func getProtoMessageAggregateSize(c *sizeContext, m protoreflect.Message) uint32 { + if !m.IsValid() { + return 1 + } + if !c.visitNode() { + return math.MaxUint32 + } + total := uint32(1) + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + total = safeAddUint32(total, getProtoFieldAggregateSize(c, fd, v)) + return true + }) + return total +} + +func getProtoListAggregateSize(c *sizeContext, l protoreflect.List) uint32 { + if !l.IsValid() { + return 1 + } + if !c.visitNode() { + return math.MaxUint32 + } + total := uint32(1) + for i := range l.Len() { + total = safeAddUint32(total, c.AggregateSize(l.Get(i))) + } + return total +} + +func getProtoMapAggregateSize(c *sizeContext, m protoreflect.Map) uint32 { + if !m.IsValid() { + return 1 + } + if !c.visitNode() { + return math.MaxUint32 + } + total := uint32(1) + m.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + total = safeAddUint32(total, c.AggregateSize(k)) + total = safeAddUint32(total, c.AggregateSize(v)) + return true + }) + return total +} + +// getSliceElementsAggregateSize computes the aggregate size of known slice payloads without reflection. +func getSliceElementsAggregateSize(sizer AggregateSizer, val any) (uint32, bool) { + switch v := val.(type) { + case []byte: + if sc, ok := sizer.(*sizeContext); ok { + return sc.calc.stringSize(len(v)), true + } + return 1, true + case []int: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []int8: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []int16: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []int32: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []int64: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []uint: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []uint16: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []uint32: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []uint64: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []float32: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []float64: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []bool: + return safeAddUint32(1, safeUint32FromInt(len(v))), true + case []string: + total := uint32(1) + for _, s := range v { + total = safeAddUint32(total, sizer.AggregateSize(s)) + } + return total, true + case []ref.Val: + total := uint32(1) + for _, elem := range v { + total = safeAddUint32(total, sizer.AggregateSize(elem)) + } + return total, true + case protoreflect.List: + total := uint32(1) + for i := range v.Len() { + total = safeAddUint32(total, sizer.AggregateSize(v.Get(i))) + } + return total, true + case *structpb.ListValue: + if v == nil { + return 1, true + } + total := uint32(1) + for _, elem := range v.GetValues() { + total = safeAddUint32(total, sizer.AggregateSize(elem)) + } + return total, true + case []any: + total := uint32(1) + for _, elem := range v { + total = safeAddUint32(total, sizer.AggregateSize(elem)) + } + return total, true + default: + return 0, false + } +} + +// getMapElementsAggregateSize computes the aggregate size of a map payload. +func getMapElementsAggregateSize(sizer AggregateSizer, val any) (uint32, bool) { + switch v := val.(type) { + case map[string]string: + total := uint32(1) + for k, val := range v { + total = safeAddUint32(total, sizer.AggregateSize(k)) + total = safeAddUint32(total, sizer.AggregateSize(val)) + } + return total, true + case map[string]any: + total := uint32(1) + for k, val := range v { + total = safeAddUint32(total, sizer.AggregateSize(k)) + total = safeAddUint32(total, sizer.AggregateSize(val)) + } + return total, true + case map[ref.Val]ref.Val: + total := uint32(1) + for k, val := range v { + total = safeAddUint32(total, sizer.AggregateSize(k)) + total = safeAddUint32(total, sizer.AggregateSize(val)) + } + return total, true + case *structpb.Struct: + if v == nil { + return 1, true + } + total := uint32(1) + for k, val := range v.GetFields() { + total = safeAddUint32(total, sizer.AggregateSize(k)) + total = safeAddUint32(total, sizer.AggregateSize(val)) + } + return total, true + default: + return 0, false + } +} + +func getReflectValueAggregateSize(c *sizeContext, fieldVal reflect.Value) uint32 { + if !fieldVal.IsValid() { + return 1 + } + if !c.visitNode() { + return math.MaxUint32 + } + switch fieldVal.Kind() { + case reflect.String: + return c.calc.stringSize(fieldVal.Len()) + case reflect.Slice: + elemType := fieldVal.Type().Elem() + if elemType.Kind() == reflect.Uint8 { + return c.calc.stringSize(fieldVal.Len()) + } + if fieldVal.CanInterface() { + if total, ok := getSliceElementsAggregateSize(c, fieldVal.Interface()); ok { + return total + } + } + total := safeAddUint32(1, safeUint32FromInt(fieldVal.Len())) + switch elemType.Kind() { + case reflect.String, reflect.Struct, reflect.Pointer, reflect.Slice, reflect.Array, reflect.Map, reflect.Interface: + total = 1 + for i := 0; i < fieldVal.Len(); i++ { + total = safeAddUint32(total, c.AggregateSize(fieldVal.Index(i))) + } + } + return total + case reflect.Array: + elemType := fieldVal.Type().Elem() + if elemType.Kind() == reflect.Uint8 { + return c.calc.stringSize(fieldVal.Len()) + } + total := safeAddUint32(1, safeUint32FromInt(fieldVal.Len())) + switch elemType.Kind() { + case reflect.String, reflect.Struct, reflect.Pointer, reflect.Slice, reflect.Array, reflect.Map, reflect.Interface: + total = 1 + for i := 0; i < fieldVal.Len(); i++ { + total = safeAddUint32(total, c.AggregateSize(fieldVal.Index(i))) + } + } + return total + case reflect.Map: + if fieldVal.CanInterface() { + if total, ok := getMapElementsAggregateSize(c, fieldVal.Interface()); ok { + return total + } + } + total := uint32(1) + iter := fieldVal.MapRange() + for iter.Next() { + total = safeAddUint32(total, c.AggregateSize(iter.Key())) + total = safeAddUint32(total, c.AggregateSize(iter.Value())) + } + return total + case reflect.Pointer, reflect.Interface: + if fieldVal.IsNil() { + return 1 + } + if sz, ok := checkCustomSizer(c, fieldVal); ok { + return sz + } + return getReflectValueAggregateSize(c, fieldVal.Elem()) + case reflect.Struct: + if fieldVal.Type() == timestampType || fieldVal.Type() == durationType { + return 1 + } + if sz, ok := checkCustomSizer(c, fieldVal); ok { + return sz + } + total := uint32(1) + t := fieldVal.Type() + numFields := fieldVal.NumField() + for i := range numFields { + if !t.Field(i).IsExported() { + continue + } + fVal := fieldVal.Field(i) + if !fVal.IsValid() || fVal.IsZero() { + continue + } + total = safeAddUint32(total, c.AggregateSize(fVal)) + } + return total + default: + return 1 + } +} + +var ( + aggregateSizeVisitorType = reflect.TypeFor[AggregateSizeVisitor]() + sizerType = reflect.TypeFor[traits.Sizer]() + protoMessageType = reflect.TypeFor[proto.Message]() +) + +func checkCustomSizer(c *sizeContext, fieldVal reflect.Value) (uint32, bool) { + if !fieldVal.IsValid() || !fieldVal.CanInterface() { + return 0, false + } + t := fieldVal.Type() + if t.Implements(aggregateSizeVisitorType) { + if sizer, ok := fieldVal.Interface().(AggregateSizeVisitor); ok { + return sizer.AggregateSize(c), true + } + } + if t.Implements(sizerType) { + if sizer, ok := fieldVal.Interface().(traits.Sizer); ok { + return max(1, safeUint32FromBoxedInt(sizer.Size().(Int))), true + } + } + if t.Implements(protoMessageType) { + if fieldVal.Kind() == reflect.Pointer && fieldVal.IsNil() { + return 1, true + } + if sizer, ok := fieldVal.Interface().(proto.Message); ok { + if sizer == nil { + return 1, true + } + return getProtoMessageAggregateSize(c, sizer.ProtoReflect()), true + } + } + return 0, false +} diff --git a/vendor/github.com/google/cel-go/common/types/string.go b/vendor/cel.dev/cel-go/common/types/string.go similarity index 86% rename from vendor/github.com/google/cel-go/common/types/string.go rename to vendor/cel.dev/cel-go/common/types/string.go index 5f5a43358ea..e2dde05bd69 100644 --- a/vendor/github.com/google/cel-go/common/types/string.go +++ b/vendor/cel.dev/cel-go/common/types/string.go @@ -22,8 +22,8 @@ import ( "strings" "time" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" @@ -41,7 +41,7 @@ var ( overloads.StartsWith: StringStartsWith, } - stringWrapperType = reflect.TypeOf(&wrapperspb.StringValue{}) + stringWrapperType = reflect.TypeFor[*wrapperspb.StringValue]() ) // Add implements traits.Adder.Add. @@ -59,7 +59,7 @@ func (s String) Compare(other ref.Val) ref.Val { if !ok { return MaybeNoSuchOverloadErr(other) } - return Int(strings.Compare(s.Value().(string), otherString.Value().(string))) + return Int(strings.Compare(string(s), string(otherString))) } // ConvertToNative implements ref.Val.ConvertToNative. @@ -80,7 +80,7 @@ func (s String) ConvertToNative(typeDesc reflect.Type) (any, error) { return wrapperspb.String(string(s)), nil } if typeDesc.Elem().Kind() == reflect.String { - p := s.Value().(string) + p := string(s) return &p, nil } case reflect.Interface: @@ -100,29 +100,33 @@ func (s String) ConvertToNative(typeDesc reflect.Type) (any, error) { func (s String) ConvertToType(typeVal ref.Type) ref.Val { switch typeVal { case IntType: - if n, err := strconv.ParseInt(s.Value().(string), 10, 64); err == nil { + if n, err := strconv.ParseInt(string(s), 10, 64); err == nil { return Int(n) } case UintType: - if n, err := strconv.ParseUint(s.Value().(string), 10, 64); err == nil { + if n, err := strconv.ParseUint(string(s), 10, 64); err == nil { return Uint(n) } case DoubleType: - if n, err := strconv.ParseFloat(s.Value().(string), 64); err == nil { + if n, err := strconv.ParseFloat(string(s), 64); err == nil { return Double(n) } case BoolType: - if b, err := strconv.ParseBool(s.Value().(string)); err == nil { + if b, err := strconv.ParseBool(string(s)); err == nil { return Bool(b) } case BytesType: return Bytes(s) case DurationType: - if d, err := time.ParseDuration(s.Value().(string)); err == nil { + if d, err := time.ParseDuration(string(s)); err == nil { return durationOf(d) } case TimestampType: - if t, err := time.Parse(time.RFC3339, s.Value().(string)); err == nil { + str := string(s) + if !isStrictRFC3339(str) { + return NewErr("invalid RFC 3339 timestamp %q", str) + } + if t, err := time.Parse(time.RFC3339, str); err == nil { if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { return celErrTimestampOverflow } @@ -153,7 +157,7 @@ func (s String) Match(pattern ref.Val) ref.Val { if !ok { return MaybeNoSuchOverloadErr(pattern) } - matched, err := regexp.MatchString(pat.Value().(string), s.Value().(string)) + matched, err := regexp.MatchString(string(pat), string(s)) if err != nil { return &Err{error: err} } @@ -173,7 +177,7 @@ func (s String) Receive(function string, overload string, args []ref.Val) ref.Va // Size implements traits.Sizer.Size. func (s String) Size() ref.Val { - return Int(len([]rune(s.Value().(string)))) + return Int(len([]rune(s))) } // Type implements ref.Val.Type. diff --git a/vendor/cel.dev/cel-go/common/types/struct.go b/vendor/cel.dev/cel-go/common/types/struct.go new file mode 100644 index 00000000000..0b50895a524 --- /dev/null +++ b/vendor/cel.dev/cel-go/common/types/struct.go @@ -0,0 +1,39 @@ +// 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 types + +import ( + "reflect" + + "cel.dev/cel-go/common/types/ref" +) + +// StructTypeDescriptor describes a CEL struct type, providing field metadata and value instantiation. +type StructTypeDescriptor interface { + // ReflectType returns the backing Go reflect.Type associated with the struct (or nil if non-reflected). + ReflectType() reflect.Type + + // FieldNames returns the list of field names defined on the struct. + FieldNames() []string + + // FindFieldType returns the field type and a boolean indicating if the field exists. + FindFieldType(fieldName string) (*FieldType, bool) + + // NewValue creates a new CEL struct value from the given map of field values. + NewValue(adapter Adapter, fields map[string]ref.Val) ref.Val + + // Adapt converts a native Go value (struct instance or pointer) to a CEL ref.Val. + Adapt(adapter Adapter, value any) ref.Val +} diff --git a/vendor/github.com/google/cel-go/common/types/timestamp.go b/vendor/cel.dev/cel-go/common/types/timestamp.go similarity index 62% rename from vendor/github.com/google/cel-go/common/types/timestamp.go rename to vendor/cel.dev/cel-go/common/types/timestamp.go index c4484291bee..7b316c6d35d 100644 --- a/vendor/github.com/google/cel-go/common/types/timestamp.go +++ b/vendor/cel.dev/cel-go/common/types/timestamp.go @@ -15,14 +15,18 @@ package types import ( + "encoding/json" + "errors" "fmt" "reflect" + "regexp" "strconv" "strings" "time" + "unicode" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" @@ -52,6 +56,79 @@ const ( maxUnixTime int64 = 253402300799 ) +// strictRFC3339Pattern gates the strings accepted by the `timestamp()` overload. +// time.Parse accepts inputs that RFC 3339 forbids: a ',' fractional-second +// separator, single-digit time fields, and numeric offsets whose hours exceed +// 23 or minutes exceed 59. Those slip past unnoticed and shift the parsed +// instant, so they are rejected before time.Parse runs. Month and day are held +// to the grammar ranges 01-12 and 01-31; the remaining calendar validation +// (day-of-month vs. month, leap years) is left to time.Parse. +// +// isStrictRFC3339 is the implementation used on the conversion path; the pattern +// is retained as the reference the scan is conformance tested against. +var strictRFC3339Pattern = regexp.MustCompile( + `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$`) + +// isStrictRFC3339 reports whether s matches strictRFC3339Pattern, hand-rolled to +// keep the conversion path off the regexp engine and its per-call cost. +func isStrictRFC3339(s string) bool { + // Shortest accepted form is "2006-01-02T15:04:05Z" (20 bytes): a 19-byte + // fixed-width date-time followed by at least a 'Z'/'z' zone. + if len(s) < 20 { + return false + } + // full-date "T" partial-time + if !isYear(s[0:4]) || !isChar(s[4], '-') || !isMonth(s[5:7]) || !isChar(s[7], '-') || !isDay(s[8:10]) || + !isChar(s[10], 't') || + !isHour(s[11:13]) || !isChar(s[13], ':') || !isMinute(s[14:16]) || !isChar(s[16], ':') || !isSecond(s[17:19]) { + return false + } + rest := s[19:] + // optional fractional seconds: "." 1*DIGIT + if rest[0] == '.' { + rest = rest[1:] + n := 0 + for n < len(rest) && isDigit(rest[n]) { + n++ + } + if n == 0 { + return false + } + rest = rest[n:] + } + // time-offset: "Z" or ("+" / "-") time-hour ":" time-minute + if len(rest) == 1 { + return isChar(rest[0], 'z') + } + if len(rest) == 6 && (rest[0] == '+' || rest[0] == '-') { + return isHour(rest[1:3]) && isChar(rest[3], ':') && isMinute(rest[4:6]) + } + return false +} + +func isDigit(c byte) bool { return c >= '0' && c <= '9' } + +// isChar reports whether got is want, case-insensitively; want must be lower case. +func isChar(got, want byte) bool { + g, w := rune(got), rune(want) + return g == w || unicode.ToLower(g) == w +} + +// inRange reports whether s is all decimal digits and its value lies in [lo, hi]. +func inRange(s string, lo, hi uint64) bool { + u, err := strconv.ParseUint(s, 10, 64) + return err == nil && u >= lo && u <= hi +} + +func isYear(s string) bool { return inRange(s, 0, 9999) } +func isMonth(s string) bool { return inRange(s, 1, 12) } +func isDay(s string) bool { return inRange(s, 1, 31) } +func isHour(s string) bool { return inRange(s, 0, 23) } +func isMinute(s string) bool { return inRange(s, 0, 59) } + +// isSecond permits 60 for a leap second. +func isSecond(s string) bool { return inRange(s, 0, 60) } + // Add implements traits.Adder.Add. func (t Timestamp) Add(other ref.Val) ref.Val { switch other.Type() { @@ -183,6 +260,90 @@ func (t Timestamp) format(sb *strings.Builder) { fmt.Fprintf(sb, `timestamp("%s")`, t.Time.UTC().Format(time.RFC3339Nano)) } +// ParseTimestamp attempts to parse a timestamp from various supported types and representations: +// - time.Time, Timestamp, *timestamppb.Timestamp +// - RFC 3339 and RFC 3339Nano formatted strings (e.g. "2023-01-01T00:00:00Z") +// - Unix epoch integers (int, int32, int64) +// - Unix epoch floating-point seconds (float32, float64) +// - json.Number +// - String representations of integers or floating-point epoch seconds +// +// If the parsed timestamp falls outside the supported range [minUnixTime, maxUnixTime], an error is returned. +func ParseTimestamp(val any) (time.Time, error) { + if val == nil { + return time.Time{}, errors.New("invalid timestamp: nil value") + } + switch v := val.(type) { + case time.Time: + return validateTimestampRange(v.UTC()) + case Timestamp: + return validateTimestampRange(v.Time.UTC()) + case *tpb.Timestamp: + if v == nil { + return time.Time{}, nil + } + return validateTimestampRange(v.AsTime().UTC()) + case int: + return validateTimestampRange(time.Unix(int64(v), 0).UTC()) + case int32: + return validateTimestampRange(time.Unix(int64(v), 0).UTC()) + case int64: + return validateTimestampRange(time.Unix(v, 0).UTC()) + case float32: + return unixTimeFromFloat(float64(v)) + case float64: + return unixTimeFromFloat(v) + case json.Number: + if i, err := v.Int64(); err == nil { + return validateTimestampRange(time.Unix(i, 0).UTC()) + } + if f, err := v.Float64(); err == nil { + return unixTimeFromFloat(f) + } + return ParseTimestamp(v.String()) + case string: + s := strings.TrimSpace(v) + if s == "" { + return time.Time{}, errors.New("invalid RFC 3339 timestamp: ''") + } + if isStrictRFC3339(s) { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return time.Time{}, fmt.Errorf("invalid RFC 3339 timestamp %q", s) + } + return validateTimestampRange(t.UTC()) + } + if i, err := strconv.ParseInt(s, 10, 64); err == nil { + return validateTimestampRange(time.Unix(i, 0).UTC()) + } + if f, err := strconv.ParseFloat(s, 64); err == nil { + return unixTimeFromFloat(f) + } + return time.Time{}, fmt.Errorf("unsupported timestamp format: %q", s) + default: + return time.Time{}, fmt.Errorf("unsupported timestamp type: %T", val) + } +} + +func unixTimeFromFloat(f float64) (time.Time, error) { + sec, err := doubleToInt64Checked(f) + if err != nil { + return time.Time{}, err + } + nsec := int64((f - float64(sec)) * 1e9) + return validateTimestampRange(time.Unix(sec, nsec).UTC()) +} + +func validateTimestampRange(t time.Time) (time.Time, error) { + if t.IsZero() { + return t, nil + } + if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { + return time.Time{}, fmt.Errorf("timestamp overflow: %v", t) + } + return t, nil +} + var ( timestampValueType = reflect.TypeOf(&tpb.Timestamp{}) @@ -293,7 +454,7 @@ func timeZone(tz ref.Val, visitor timestampVisitor) timestampVisitor { } // If the input is not the name of a timezone (for example, 'US/Central'), it should be a numerical offset from UTC - // in the format ^(+|-)(0[0-9]|1[0-4]):[0-5][0-9]$. The numerical input is parsed in terms of hours and minutes. + // in the format ^(+|-)([01]\d|2[0-3]):[0-5][0-9]$. The numerical input is parsed in terms of hours and minutes. hr, err := strconv.Atoi(string(val[0:ind])) if err != nil { return WrapErr(err) @@ -302,6 +463,9 @@ func timeZone(tz ref.Val, visitor timestampVisitor) timestampVisitor { if err != nil { return WrapErr(err) } + if hr < -23 || hr > 23 { + return WrapErr(fmt.Errorf("timezone offset hours out of range [-23, 23]: %s", val)) + } if min < 0 || min > 59 { return WrapErr(fmt.Errorf("timezone offset minutes out of range [0, 59]: %s", val)) } diff --git a/vendor/github.com/google/cel-go/common/types/traits/BUILD.bazel b/vendor/cel.dev/cel-go/common/types/traits/BUILD.bazel similarity index 89% rename from vendor/github.com/google/cel-go/common/types/traits/BUILD.bazel rename to vendor/cel.dev/cel-go/common/types/traits/BUILD.bazel index b19eb8301e2..e2f7d6fdb51 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/BUILD.bazel +++ b/vendor/cel.dev/cel-go/common/types/traits/BUILD.bazel @@ -22,7 +22,7 @@ go_library( "traits.go", "zeroer.go", ], - importpath = "github.com/google/cel-go/common/types/traits", + importpath = "cel.dev/cel-go/common/types/traits", deps = [ "//common/types/ref:go_default_library", ], diff --git a/vendor/github.com/google/cel-go/common/types/traits/comparer.go b/vendor/cel.dev/cel-go/common/types/traits/comparer.go similarity index 95% rename from vendor/github.com/google/cel-go/common/types/traits/comparer.go rename to vendor/cel.dev/cel-go/common/types/traits/comparer.go index b531d9ae2bf..83b1395354b 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/comparer.go +++ b/vendor/cel.dev/cel-go/common/types/traits/comparer.go @@ -15,7 +15,7 @@ package traits import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // Comparer interface for ordering comparisons between values in order to diff --git a/vendor/github.com/google/cel-go/common/types/traits/container.go b/vendor/cel.dev/cel-go/common/types/traits/container.go similarity index 94% rename from vendor/github.com/google/cel-go/common/types/traits/container.go rename to vendor/cel.dev/cel-go/common/types/traits/container.go index cf5c621ae9f..357ccd31d41 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/container.go +++ b/vendor/cel.dev/cel-go/common/types/traits/container.go @@ -14,7 +14,7 @@ package traits -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // Container interface which permits containment tests such as 'a in b'. type Container interface { diff --git a/vendor/github.com/google/cel-go/common/types/traits/field_tester.go b/vendor/cel.dev/cel-go/common/types/traits/field_tester.go similarity index 95% rename from vendor/github.com/google/cel-go/common/types/traits/field_tester.go rename to vendor/cel.dev/cel-go/common/types/traits/field_tester.go index 816a9565238..d80351b40b7 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/field_tester.go +++ b/vendor/cel.dev/cel-go/common/types/traits/field_tester.go @@ -15,7 +15,7 @@ package traits import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // FieldTester indicates if a defined field on an object type is set to a diff --git a/vendor/github.com/google/cel-go/common/types/traits/indexer.go b/vendor/cel.dev/cel-go/common/types/traits/indexer.go similarity index 94% rename from vendor/github.com/google/cel-go/common/types/traits/indexer.go rename to vendor/cel.dev/cel-go/common/types/traits/indexer.go index 662c6836c33..2d1f72ae227 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/indexer.go +++ b/vendor/cel.dev/cel-go/common/types/traits/indexer.go @@ -15,7 +15,7 @@ package traits import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // Indexer permits random access of elements by index 'a[b()]'. diff --git a/vendor/github.com/google/cel-go/common/types/traits/iterator.go b/vendor/cel.dev/cel-go/common/types/traits/iterator.go similarity index 97% rename from vendor/github.com/google/cel-go/common/types/traits/iterator.go rename to vendor/cel.dev/cel-go/common/types/traits/iterator.go index 91c10f08fcf..f0a0b4cb9fd 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/iterator.go +++ b/vendor/cel.dev/cel-go/common/types/traits/iterator.go @@ -15,7 +15,7 @@ package traits import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // Iterable aggregate types permit traversal over their elements. diff --git a/vendor/github.com/google/cel-go/common/types/traits/lister.go b/vendor/cel.dev/cel-go/common/types/traits/lister.go similarity index 95% rename from vendor/github.com/google/cel-go/common/types/traits/lister.go rename to vendor/cel.dev/cel-go/common/types/traits/lister.go index e54781a602f..67f32391b06 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/lister.go +++ b/vendor/cel.dev/cel-go/common/types/traits/lister.go @@ -14,7 +14,7 @@ package traits -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // Lister interface which aggregates the traits of a list. type Lister interface { diff --git a/vendor/github.com/google/cel-go/common/types/traits/mapper.go b/vendor/cel.dev/cel-go/common/types/traits/mapper.go similarity index 96% rename from vendor/github.com/google/cel-go/common/types/traits/mapper.go rename to vendor/cel.dev/cel-go/common/types/traits/mapper.go index d13333f3f60..7a4acedc54f 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/mapper.go +++ b/vendor/cel.dev/cel-go/common/types/traits/mapper.go @@ -14,7 +14,7 @@ package traits -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // Mapper interface which aggregates the traits of a maps. type Mapper interface { diff --git a/vendor/github.com/google/cel-go/common/types/traits/matcher.go b/vendor/cel.dev/cel-go/common/types/traits/matcher.go similarity index 93% rename from vendor/github.com/google/cel-go/common/types/traits/matcher.go rename to vendor/cel.dev/cel-go/common/types/traits/matcher.go index 085dc94ff4f..4ea283e908f 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/matcher.go +++ b/vendor/cel.dev/cel-go/common/types/traits/matcher.go @@ -14,7 +14,7 @@ package traits -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // Matcher interface for supporting 'matches()' overloads. type Matcher interface { diff --git a/vendor/github.com/google/cel-go/common/types/traits/math.go b/vendor/cel.dev/cel-go/common/types/traits/math.go similarity index 97% rename from vendor/github.com/google/cel-go/common/types/traits/math.go rename to vendor/cel.dev/cel-go/common/types/traits/math.go index 86d5b9137e6..cb6354f7ee6 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/math.go +++ b/vendor/cel.dev/cel-go/common/types/traits/math.go @@ -14,7 +14,7 @@ package traits -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // Adder interface to support '+' operator overloads. type Adder interface { diff --git a/vendor/github.com/google/cel-go/common/types/traits/receiver.go b/vendor/cel.dev/cel-go/common/types/traits/receiver.go similarity index 94% rename from vendor/github.com/google/cel-go/common/types/traits/receiver.go rename to vendor/cel.dev/cel-go/common/types/traits/receiver.go index 8f41db45e8f..4760192c686 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/receiver.go +++ b/vendor/cel.dev/cel-go/common/types/traits/receiver.go @@ -14,7 +14,7 @@ package traits -import "github.com/google/cel-go/common/types/ref" +import "cel.dev/cel-go/common/types/ref" // Receiver interface for routing instance method calls within a value. type Receiver interface { diff --git a/vendor/github.com/google/cel-go/common/types/traits/sizer.go b/vendor/cel.dev/cel-go/common/types/traits/sizer.go similarity index 94% rename from vendor/github.com/google/cel-go/common/types/traits/sizer.go rename to vendor/cel.dev/cel-go/common/types/traits/sizer.go index b80d25137a7..8e2e3cf926d 100644 --- a/vendor/github.com/google/cel-go/common/types/traits/sizer.go +++ b/vendor/cel.dev/cel-go/common/types/traits/sizer.go @@ -15,7 +15,7 @@ package traits import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // Sizer interface for supporting 'size()' overloads. diff --git a/vendor/github.com/google/cel-go/common/types/traits/traits.go b/vendor/cel.dev/cel-go/common/types/traits/traits.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/traits/traits.go rename to vendor/cel.dev/cel-go/common/types/traits/traits.go diff --git a/vendor/github.com/google/cel-go/common/types/traits/zeroer.go b/vendor/cel.dev/cel-go/common/types/traits/zeroer.go similarity index 100% rename from vendor/github.com/google/cel-go/common/types/traits/zeroer.go rename to vendor/cel.dev/cel-go/common/types/traits/zeroer.go diff --git a/vendor/github.com/google/cel-go/common/types/types.go b/vendor/cel.dev/cel-go/common/types/types.go similarity index 96% rename from vendor/github.com/google/cel-go/common/types/types.go rename to vendor/cel.dev/cel-go/common/types/types.go index 78c77a9b5d3..ea3fe21421b 100644 --- a/vendor/github.com/google/cel-go/common/types/types.go +++ b/vendor/cel.dev/cel-go/common/types/types.go @@ -21,9 +21,9 @@ import ( "google.golang.org/protobuf/proto" - chkdecls "github.com/google/cel-go/checker/decls" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + chkdecls "cel.dev/cel-go/checker/decls" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" celpb "cel.dev/expr" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" @@ -412,12 +412,31 @@ func (t *Type) String() string { return fmt.Sprintf("%s(%s)", t.DeclaredTypeName(), strings.Join(params, ", ")) } +// IsDyn indicates whether the type is dynamic in any way. +func (t *Type) IsDyn() bool { + return t.isDyn() +} + // isDyn indicates whether the type is dynamic in any way. func (t *Type) isDyn() bool { k := t.Kind() return k == DynKind || k == AnyKind || k == TypeParamKind } +// IsErrOrUnknown indicates whether the type is an error or unknown type. +func (t *Type) IsErrOrUnknown() bool { + return t.isErrOrUnknown() +} + +// isErrOrUnknown indicates whether the type is an error or unknown type. +func (t *Type) isErrOrUnknown() bool { + if t == nil { + return false + } + k := t.Kind() + return k == ErrorKind || k == UnknownKind +} + // defaultIsAssignableType provides the standard definition of what it means for one type to be assignable to another // where any of the following may return a true result: // - The from types are the same instance @@ -426,7 +445,11 @@ func (t *Type) isDyn() bool { // // are IsAssignableType() from the parameters of the fromType. func (t *Type) defaultIsAssignableType(fromType *Type) bool { - if t == fromType || t.isDyn() { + if t == fromType { + return true + } + // if t is dynamic, it is assignable to any type except error or unknown + if t.isDyn() && !fromType.isErrOrUnknown() { return true } if t.Kind() != fromType.Kind() || @@ -446,9 +469,12 @@ func (t *Type) defaultIsAssignableType(fromType *Type) bool { // defaultIsAssignableRuntimeType inspects the type and in the case of list and map elements, the key and element types // to determine whether a ref.Val is assignable to the declared type for a function signature. func (t *Type) defaultIsAssignableRuntimeType(val ref.Val) bool { - valType := val.Type() + rawType := val.Type() + if valType, ok := rawType.(*Type); ok && valType.isErrOrUnknown() { + return false + } // If the current type and value type don't agree, then return - if !(t.isDyn() || t.TypeName() == valType.TypeName()) { + if !(t.isDyn() || t.TypeName() == rawType.TypeName()) { return false } switch t.Kind() { diff --git a/vendor/github.com/google/cel-go/common/types/uint.go b/vendor/cel.dev/cel-go/common/types/uint.go similarity index 99% rename from vendor/github.com/google/cel-go/common/types/uint.go rename to vendor/cel.dev/cel-go/common/types/uint.go index 91d5369daae..6c2bef7299c 100644 --- a/vendor/github.com/google/cel-go/common/types/uint.go +++ b/vendor/cel.dev/cel-go/common/types/uint.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" anypb "google.golang.org/protobuf/types/known/anypb" structpb "google.golang.org/protobuf/types/known/structpb" diff --git a/vendor/github.com/google/cel-go/common/types/unknown.go b/vendor/cel.dev/cel-go/common/types/unknown.go similarity index 98% rename from vendor/github.com/google/cel-go/common/types/unknown.go rename to vendor/cel.dev/cel-go/common/types/unknown.go index 24d86518b40..96150fc3f2f 100644 --- a/vendor/github.com/google/cel-go/common/types/unknown.go +++ b/vendor/cel.dev/cel-go/common/types/unknown.go @@ -16,13 +16,14 @@ package types import ( "fmt" + "maps" "math" "reflect" "sort" "strings" "unicode" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) var ( @@ -297,9 +298,7 @@ func MergeUnknowns(unk1, unk2 *Unknown) *Unknown { out := &Unknown{ attributeTrails: make(map[int64][]*AttributeTrail, len(unk1.attributeTrails)+len(unk2.attributeTrails)), } - for id, ats := range unk1.attributeTrails { - out.attributeTrails[id] = ats - } + maps.Copy(out.attributeTrails, unk1.attributeTrails) for id, ats := range unk2.attributeTrails { existing, found := out.attributeTrails[id] if !found { diff --git a/vendor/github.com/google/cel-go/common/types/util.go b/vendor/cel.dev/cel-go/common/types/util.go similarity index 96% rename from vendor/github.com/google/cel-go/common/types/util.go rename to vendor/cel.dev/cel-go/common/types/util.go index 71662eee31b..cb37389f986 100644 --- a/vendor/github.com/google/cel-go/common/types/util.go +++ b/vendor/cel.dev/cel-go/common/types/util.go @@ -15,7 +15,7 @@ package types import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // IsUnknownOrError returns whether the input element ref.Val is an ErrType or UnknownType. diff --git a/vendor/github.com/google/cel-go/ext/BUILD.bazel b/vendor/cel.dev/cel-go/ext/BUILD.bazel similarity index 89% rename from vendor/github.com/google/cel-go/ext/BUILD.bazel rename to vendor/cel.dev/cel-go/ext/BUILD.bazel index f362fd97b8f..7ad1c205b87 100644 --- a/vendor/github.com/google/cel-go/ext/BUILD.bazel +++ b/vendor/cel.dev/cel-go/ext/BUILD.bazel @@ -9,7 +9,6 @@ go_library( srcs = [ "bindings.go", "comprehensions.go", - "costs.go", "encoders.go", "extension_option_factory.go", "formatting.go", @@ -24,13 +23,13 @@ go_library( "sets.go", "strings.go", ], - importpath = "github.com/google/cel-go/ext", + importpath = "cel.dev/cel-go/ext", visibility = ["//visibility:public"], deps = [ "//cel:go_default_library", - "//checker:go_default_library", "//common:go_default_library", "//common/ast:go_default_library", + "//common/cost:go_default_library", "//common/decls:go_default_library", "//common/env:go_default_library", "//common/operators:go_default_library", @@ -41,6 +40,7 @@ go_library( "//common/types/traits:go_default_library", "//interpreter:go_default_library", "//parser:go_default_library", + "@in_yaml_go_yaml_v3//: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", @@ -56,6 +56,8 @@ go_test( srcs = [ "bindings_test.go", "comprehensions_test.go", + "cost_golden_test.go", + "cost_soundness_test.go", "encoders_test.go", "extension_option_factory_test.go", "formatting_test.go", @@ -69,6 +71,9 @@ go_test( "sets_test.go", "strings_test.go", ], + data = glob([ + "testdata/**", + ]), embed = [ ":go_default_library", ], @@ -76,6 +81,7 @@ go_test( "//cel:go_default_library", "//checker:go_default_library", "//common:go_default_library", + "//common/cost:go_default_library", "//common/env:go_default_library", "//common/types:go_default_library", "//common/types/ref:go_default_library", @@ -83,6 +89,7 @@ go_test( "//test:go_default_library", "//test/proto2pb:go_default_library", "//test/proto3pb:go_default_library", + "@in_yaml_go_yaml_v3//:go_default_library", "@org_golang_google_protobuf//encoding/protojson:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library", diff --git a/vendor/github.com/google/cel-go/ext/README.md b/vendor/cel.dev/cel-go/ext/README.md similarity index 70% rename from vendor/github.com/google/cel-go/ext/README.md rename to vendor/cel.dev/cel-go/ext/README.md index 523d9085640..f296b37e8e0 100644 --- a/vendor/github.com/google/cel-go/ext/README.md +++ b/vendor/cel.dev/cel-go/ext/README.md @@ -31,8 +31,13 @@ Local bindings are not guaranteed to be evaluated before use. Encoding utilities for marshalling data into standardized representations. +Note: Version 2 of this library depends on the CEL optional type. Please ensure that +cel.OptionalTypes() is enabled when using encoder extensions at version 2 or greater. + ### Base64.Decode +**Introduced in version 0 (cost support in version 1)** + Decodes base64-encoded string to bytes. This function will return an error if the string input is not @@ -43,10 +48,29 @@ base64-encoded. Examples: base64.decode('aGVsbG8=') // return b'hello' - base64.decode('aGVsbG8') // error + base64.decode('aGVsbG8') // return b'hello' + +### Base64.DecodeUrl + +**Introduced in version 2** + +Decodes base64url-encoded string to bytes. + +This function will return an error if the string input is not +base64url-encoded. + + base64.decodeUrl() -> + +Examples: + + base64.decodeUrl('aGVsbG8=') // return b'hello' + base64.decodeUrl('aGVsbG8') // return b'hello' + base64.decodeUrl('____') // return b'\xff\xff\xff' ### Base64.Encode +**Introduced in version 0 (cost support in version 1)** + Encodes bytes to a base64-encoded string. base64.encode() -> @@ -55,6 +79,20 @@ Example: base64.encode(b'hello') // return 'aGVsbG8=' +### Base64.EncodeUrl + +**Introduced in version 2** + +Encodes bytes to a base64url-encoded string. + + base64.encodeUrl() -> + +Examples: + + base64.encodeUrl(b'hello') // return 'aGVsbG8=' + base64.encodeUrl(b'\xff\xff\xff') // return '____' + + ### JSON.Encode Introduced at version: 1 @@ -69,6 +107,48 @@ Examples: json.encode([1, 'two', true]) // return '[1,"two",true]' json.encode({'items': [1, 'two', false]}) // return '{"items":[1,"two",false]}' +### JSON.Parse + +Introduced at version: 2 + +Parses a JSON string to a CEL value or a specific type. + + json.parse() -> + json.parse(, ) -> + +Examples: + + json.parse('{"hello":"world"}') // return optional.of({'hello': 'world'}) + json.parse('123', int) // return optional.of(123) + +### YAML.Encode + +Introduced at version: 2 + +Encodes a CEL value to a YAML string. + + yaml.encode() -> + +Examples: + + yaml.encode('hello') // return "hello\n" + yaml.encode([1, 'two', true]) // return "- 1\n- two\n- true\n" + yaml.encode({'items': [1, 'two', false]}) // return "items:\n - 1\n - two\n - false\n" + +### YAML.Parse + +Introduced at version: 2 + +Parses a YAML string to a CEL value or a specific type. + + yaml.parse() -> + yaml.parse(, ) -> + +Examples: + + yaml.parse('hello: world') // return optional.of({'hello': 'world'}) + yaml.parse('123', int) // return optional.of(123) + ## Math Math helper macros and functions. @@ -80,6 +160,8 @@ intended; however, there is some chance for collision. ### Math.Greatest +**Introduced in version 0 (cost support in version 3)** + Returns the greatest valued number present in the arguments to the macro. Greatest is a variable argument count macro which must take at least one @@ -107,6 +189,8 @@ Examples: ### Math.Least +**Introduced in version 0 (cost support in version 3)** + Returns the least valued number present in the arguments to the macro. Least is a variable argument count macro which must take at least one @@ -419,6 +503,208 @@ Example: proto.hasExt(msg, google.expr.proto2.test.int32_ext) // returns true || false +## Optionals + +Optionals introduce the `optional(T)` type: a value which either holds a value +of type `T`, or holds no value at all. Optionals make it possible to express +whether a value is present without resorting to a sentinel value, and to +recover from an absent field, map key, or list index without an error. + +Unlike the other extensions described here, optionals are not part of the `ext` +package. They are enabled with the `cel.OptionalTypes()` environment option: + + cel.NewEnv(cel.OptionalTypes()) + +The set of available functions may be limited to a particular version of the +library with `cel.OptionalTypes(cel.OptionalTypesVersion())`. Where a +function below does not state the version in which it was introduced, it has +been present since the library was first created. + +The `first()` and `last()` list functions are also introduced by this library +and are documented in the [Lists](#lists) section. + +### Optional Syntax + +Enabling the library also enables optional syntax in the parser. Field +selection and indexing may be made optional with `.?` and `[?...]`, which yield +`optional.none()` rather than an error when the field, key, or index is absent. +Optional field selection is viral: every selection after the first is also +optional. + + msg.?field // optional.of(msg.field), else optional.none() + msg.?field.?nested_field // optional.none() if either field is absent + +Examples: + + {'key': 'value'}[?'key'] == optional.of('value') + {'key': 'value'}[?'missing'] == optional.none() + [1, 2, 3][?0] == optional.of(1) + [1, 2, 3][?3] == optional.none() + +Map and message fields, as well as list elements, may also be set optionally, +in which case the entry is only included when the optional holds a value. + + {?'key': optional.of('value')} == {'key': 'value'} + {?'key': optional.none()} == {} + [?optional.of(1), ?optional.none()] == [1] + +### Optional.Of + +Creates an optional with the given value, where any value is considered valid. + + optional.of() -> + +Examples: + + optional.of(1).value() == 1 + optional.of('').hasValue() == true + +### Optional.OfNonZeroValue + +Creates an optional with the given value if the value is not a zero or empty +value, and `optional.none()` otherwise. + + optional.ofNonZeroValue() -> + +Examples: + + optional.ofNonZeroValue('hello').value() == 'hello' + optional.ofNonZeroValue('') == optional.none() + optional.ofNonZeroValue([]) == optional.none() + optional.ofNonZeroValue(0) == optional.none() + +### Optional.None + +The singleton value representing an optional without a value. + + optional.none() -> + +Examples: + + optional.none().hasValue() == false + +### Value + +Returns the value held by the optional. Dereferencing an optional which holds +no value is an error. + + .value() -> + +Examples: + + optional.of(1).value() == 1 + optional.none().value() // error: optional.none() dereference + +### HasValue + +Determines whether the optional holds a value, and when called with an +argument, whether the value it holds is equal to that argument. + + .hasValue() -> + .hasValue() -> + +The overload which tests equality was **introduced in the OptionalTypes library +version 3**. It is syntactic sugar for +`opt.hasValue() ? opt.value() == v : false`, so an optional which holds no +value is never equal to a value and the result is `false` regardless of the +argument. + +Examples: + + optional.of(1).hasValue() == true + optional.ofNonZeroValue({}).hasValue() == false + + optional.of(1).hasValue(1) == true + optional.of(1).hasValue(2) == false + optional.none().hasValue(1) == false + {'key': 'value'}[?'key'].hasValue('value') == true + +The argument is compared against the value held by the optional and is never +unwrapped, so an `optional(T)` argument is only equal to the value of an +`optional(optional(T))`. The type-checker enforces this agreement; a `dyn` +typed argument defers the comparison to runtime, where a type mismatch is +simply unequal, just as it would be for `==`. + + optional.of(optional.of(1)).hasValue(optional.of(1)) == true + optional.of(1).hasValue(dyn(optional.of(1))) == false + +### Or + +Returns the optional on the left-hand side if it holds a value, and the +optional on the right-hand side otherwise. The call is short-circuiting: only +as many links in an `or` chain are evaluated as are needed to produce a value. + + .or() -> + +Examples: + + optional.of(1).or(optional.of(2)).value() == 1 + optional.none().or(optional.of(1)).value() == 1 + +### OrValue + +Returns the value held by the optional on the left-hand side, or the +alternative value on the right-hand side. Like `or`, the call is +short-circuiting. + + .orValue() -> + +Examples: + + optional.of(1).orValue(2) == 1 + optional.none().orValue('none') == 'none' + +### OptMap + +Applies a transformation to the optional's value if it holds one, and wraps the +result in an optional. An optional which holds no value is returned unchanged. +Starting in **OptionalTypes library version 4**, `optMap` avoids evaluating +complex target expressions more than once. The change for more +efficient evaluation alters the cost model in a backward incompatible manner, +hhence the versioned rollout. + + .optMap(, ) -> + +Examples: + + optional.of(2).optMap(i, i * 2).value() == 4 + optional.none().optMap(i, i * 2) == optional.none() + +### OptFlatMap + +**Introduced in the OptionalTypes library version 1** + +Applies a transformation to the optional's value if it holds one, and returns +the result. Unlike `optMap`, the transformation must itself produce an +optional, which makes it possible to express results that may be empty in ways +`optMap` cannot. Starting in **OptionalTypes library version 4**, `optFlatMap` +avoids evaluating complex target expressions more than once. The change for more +efficient evaluation alters the cost model in a backward incompatible manner, +hence the versioned rollout. + + .optFlatMap(, ) -> + +Examples: + + {'key': {'sub': 'value'}}.?key.optFlatMap(k, k.?sub).value() == 'value' + {'key': {}}.?key.optFlatMap(k, k.?sub) == optional.none() + +### Optional.Unwrap / UnwrapOpt + +**Introduced in the OptionalTypes library version 2** + +Returns a list of the values in the input list which hold a value, dropping +those which do not. The function may be called globally or with postfix +notation. + + optional.unwrap() -> + .unwrapOpt() -> + +Examples: + + optional.unwrap([optional.of(1), optional.none(), optional.of(2)]) == [1, 2] + [optional.of(1), optional.none()].unwrapOpt() == [1] + ## Lists Extended functions for list manipulation. As a general note, all indices are @@ -457,6 +743,78 @@ Examples: [1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]] [1,[2,[3,[4]]]].flatten(-1) // error +### HasAll + +**Introduced in version 5** + +Returns whether every element of the argument list is contained within the +target list, i.e. whether the target is a superset of the argument. Standard +CEL equality is used to determine whether two elements are equal, and neither +list is required to contain unique elements. An empty argument list always +returns true. + + .hasAll() -> + +Examples: + + [1, 2, 3].hasAll([]) // return true + [1, 2, 3, 4].hasAll([2, 3]) // return true + [1, 2.0, 3u].hasAll([1.0, 2u, 3]) // return true + [1, 2, 3].hasAll([2, 4]) // return false + +### HasAny + +**Introduced in version 5** + +Returns whether the target list has at least one element which is equal to an +element of the argument list. If either list is empty, the result is false. + + .hasAny() -> + +Examples: + + [1, 2, 3].hasAny([3, 4]) // return true + [[1], [2, 3]].hasAny([[1, 2], [2, 3.0]]) // return true + [1, 2].hasAny([3, 4]) // return false + [1].hasAny([]) // return false + +### HasExactly + +**Introduced in version 5** + +Returns whether the target and argument lists are set equivalent, i.e. every +element of the target list is equal to an element of the argument list and vice +versa. The lists may differ in size since neither is guaranteed to hold unique +elements, so size does not factor into the computation. + + .hasExactly() -> + +Examples: + + [].hasExactly([]) // return true + [1].hasExactly([1, 1]) // return true + [1].hasExactly([1u, 1.0]) // return true + [1, 2, 3].hasExactly([3u, 2.0, 1]) // return true + [1, 2].hasExactly([1, 2, 3]) // return false + +### HasOnly + +**Introduced in version 5** + +Returns whether every element of the target list is contained within the +argument list, i.e. whether the target is a subset of the argument. An empty +target list always returns true. + + .hasOnly() -> + +Examples: + + [].hasOnly([1, 2]) // return true + [1, 1, 2].hasOnly([1, 2, 3]) // return true + [1, 2.0, 3u].hasOnly([1.0, 2u, 3]) // return true + [1, 4].hasOnly([1, 2, 3]) // return false + [1].hasOnly([]) // return false + ### Range **Introduced in version 2 (cost support in version 3)** @@ -636,6 +994,51 @@ Examples: 'hello'.charAt(5) // return '' 'hello'.charAt(-1) // error +### Format + +**Introduced in version 1** + +**Revised in version 4**: the formatting behavior was updated to adhere to the +[CEL string extensions spec](https://github.com/google/cel-spec/blob/master/doc/extensions/strings.md). +Locale support was removed in this revision: the `StringsLocale` option is +ignored at version 4 and above. + +Returns a new string with substitutions being performed, printf-style. + +The valid formatting clauses are: + +- `%s` - substitutes a string. This can also be used on bools, lists, maps, bytes, + Duration and Timestamp, in addition to all numerical types (int, uint, and double). +- `%d` - substitutes an integer. +- `%f` - substitutes a double with fixed-point precision. The default precision + is 6, but this can be adjusted. +- `%e` - substitutes a double in scientific notation. The default precision is + 6, but this can be adjusted. +- `%b` - substitutes an integer with its equivalent binary string. Can also be + used on bools. +- `%x` - substitutes an integer with its equivalent in hexadecimal, or if given a + string or bytes, outputs each character's equivalent in hexadecimal. +- `%X` - same as `%x`, but with A-F capitalized. +- `%o` - substitutes an integer with its equivalent in octal. + + .format() -> + +Examples: + + "this is a string: %s and an integer: %d".format(["str", 42]) // returns "this is a string: str and an integer: 42" + "%f".format([3.14]) // returns "3.140000" + "5 in binary: %b".format([5]) // returns "5 in binary: 101" + "26 in hex: %x".format([26]) // returns "26 in hex: 1a" + "26 in hex (uppercase): %X".format([26]) // returns "26 in hex (uppercase): 1A" + "30 in octal: %o".format([30]) // returns "30 in octal: 36" + "duration: %s".format([duration("1h45m47s")]) // returns "duration: 6347s" + +Passing an incorrect type (a string to `%b`) is an error, as is attempting to +use more formatting clauses than there are arguments. If compile-time checking +is enabled, and the formatting string is a constant, and the argument list is a +literal, then letting any arguments go unused/unformatted is also considered +an error. + ### IndexOf Returns the integer index of the first occurrence of the search string. If the diff --git a/vendor/github.com/google/cel-go/ext/bindings.go b/vendor/cel.dev/cel-go/ext/bindings.go similarity index 81% rename from vendor/github.com/google/cel-go/ext/bindings.go rename to vendor/cel.dev/cel-go/ext/bindings.go index 89766d60aa9..4a04f44d158 100644 --- a/vendor/github.com/google/cel-go/ext/bindings.go +++ b/vendor/cel.dev/cel-go/ext/bindings.go @@ -22,12 +22,12 @@ import ( "strings" "sync" - "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" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/interpreter" ) // Bindings returns a cel.EnvOption to configure support for local variable @@ -123,6 +123,9 @@ func (lib *celBindings) ProgramOptions() []cel.ProgramOption { // Non-empty block if block, ok := args[0].(interpreter.InterpretableConstructor); ok { slotExprs := block.InitVals() + if len(slotExprs) == 0 { + return expr, nil + } return newDynamicBlock(slotExprs, expr), nil } // Constant valued block which can happen during runtime optimization. @@ -198,15 +201,11 @@ func newDynamicBlock(slotExprs []interpreter.InterpretableV2, expr interpreter.I bs.slotActivationPool = &sync.Pool{ New: func() any { slotCount := len(slotExprs) - sa := &dynamicSlotActivation{ + return &dynamicSlotActivation{ slotExprs: slotExprs, slotCount: slotCount, - slotVals: make([]*slotVal, slotCount), + slotVals: make([]slotVal, slotCount), } - for i := 0; i < slotCount; i++ { - sa.slotVals[i] = &slotVal{} - } - return sa }, } return bs @@ -228,18 +227,15 @@ func (b *dynamicBlock) ID() int64 { 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) + res := b.expr.Exec(sa.frame) + sa.frame.Pop() + b.clearSlots(sa) + return res } // Eval implements the Interpretable interface method. func (b *dynamicBlock) Eval(activation cel.Activation) ref.Val { - return b.Exec(interpreter.AsFrame(activation)) + return interpreter.EvalActivation(activation, b.Exec) } func (b *dynamicBlock) clearSlots(sa *dynamicSlotActivation) { @@ -248,21 +244,38 @@ func (b *dynamicBlock) clearSlots(sa *dynamicSlotActivation) { } type slotVal struct { - value *ref.Val - visited bool + value ref.Val + visited bool + evaluated bool } type dynamicSlotActivation struct { - cel.Activation frame *interpreter.ExecutionFrame slotExprs []interpreter.InterpretableV2 slotCount int - slotVals []*slotVal + slotVals []slotVal } -// Unwrap returns the underlying activation. +// Unwrap returns the underlying activation outside the block. func (sa *dynamicSlotActivation) Unwrap() cel.Activation { - return sa.Activation + if sa.frame != nil { + return sa.frame.Parent() + } + return nil +} + +// Parent returns the parent activation outside the block. +func (sa *dynamicSlotActivation) Parent() cel.Activation { + if sa.frame != nil { + return sa.frame.Parent() + } + return nil +} + +// IsLocalVariable reports whether the variable is locally bound to a block slot. +func (sa *dynamicSlotActivation) IsLocalVariable(name string) bool { + _, found := matchSlot(name, sa.slotCount) + return found } // ResolveName implements the Activation interface method but handles variables prefixed with `@index` @@ -270,29 +283,22 @@ func (sa *dynamicSlotActivation) Unwrap() cel.Activation { // refers to an expression which must be computed only once. func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) { if idx, found := matchSlot(name, sa.slotCount); found { - v := sa.slotVals[idx] + v := &sa.slotVals[idx] if v.visited { - // Return not found if the index expression refers to itself - if v.value == nil { - return nil, false - } - return *v.value, true + return v.value, v.evaluated } v.visited = true val := sa.slotExprs[idx].Exec(sa.frame) - v.value = &val + v.value = val + v.evaluated = true return val, true } - return sa.Activation.ResolveName(name) + return nil, false } func (sa *dynamicSlotActivation) reset() { - sa.Activation = nil sa.frame = nil - for _, sv := range sa.slotVals { - sv.visited = false - sv.value = nil - } + clear(sa.slotVals) } func newConstantBlock(slots traits.Lister, expr interpreter.InterpretableV2) interpreter.InterpretableV2 { @@ -311,40 +317,39 @@ 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. +// Exec implements the Interpretable interface method and evaluates the block with +// constant slot resolution using a child execution frame scoped to the constant slots. 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) + child := frame.Push(b) + res := b.expr.Exec(child) + child.Pop() + return res } // 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 { - return b.Exec(interpreter.AsFrame(activation)) -} - -type constantSlotActivation struct { - cel.Activation - frame *interpreter.ExecutionFrame - slots traits.Lister - slotCount int -} - -// Unwrap returns the underlying activation. -func (sa *constantSlotActivation) Unwrap() cel.Activation { - return sa.Activation + return interpreter.EvalActivation(activation, b.Exec) } // ResolveName implements Activation interface method and proxies @index prefixed lookups into the slot // activation associated with the block scope. -func (sa constantSlotActivation) ResolveName(name string) (any, bool) { - if idx, found := matchSlot(name, sa.slotCount); found { - return sa.slots.Get(types.Int(idx)), true +func (b *constantBlock) ResolveName(name string) (any, bool) { + if idx, found := matchSlot(name, b.slotCount); found { + return b.slots.Get(types.Int(idx)), true } - return sa.Activation.ResolveName(name) + return nil, false +} + +// Parent implements the Activation interface. +func (b *constantBlock) Parent() cel.Activation { + return nil +} + +// IsLocalVariable reports whether the variable is locally bound to a block slot. +func (b *constantBlock) IsLocalVariable(name string) bool { + _, found := matchSlot(name, b.slotCount) + return found } func matchSlot(name string, slotCount int) (int, bool) { diff --git a/vendor/github.com/google/cel-go/ext/comprehensions.go b/vendor/cel.dev/cel-go/ext/comprehensions.go similarity index 98% rename from vendor/github.com/google/cel-go/ext/comprehensions.go rename to vendor/cel.dev/cel-go/ext/comprehensions.go index adb22912b11..d01524bb1af 100644 --- a/vendor/github.com/google/cel-go/ext/comprehensions.go +++ b/vendor/cel.dev/cel-go/ext/comprehensions.go @@ -18,13 +18,13 @@ import ( "fmt" "math" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/parser" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/parser" ) const ( diff --git a/vendor/cel.dev/cel-go/ext/encoders.go b/vendor/cel.dev/cel-go/ext/encoders.go new file mode 100644 index 00000000000..303a39d9022 --- /dev/null +++ b/vendor/cel.dev/cel-go/ext/encoders.go @@ -0,0 +1,1150 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ext + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "go.yaml.in/yaml/v3" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +const ( + maxJSONSize = 10 * 1024 * 1024 // 10MB maximum allowed JSON string size + maxYAMLSize = 10 * 1024 * 1024 // 10MB maximum allowed YAML string size +) + +// Encoders returns a cel.EnvOption to configure extended functions for string, byte, and object +// encodings. +// +// Note: all functions use the 'base64', 'json', and 'yaml' namespaces. +// +// Version 2 of this library depends on the CEL optional type. Please ensure that the +// cel.OptionalTypes() is enabled when using encoder extensions at version 2 or greater. +// +// # Base64.Decode +// +// Decodes base64-encoded string to bytes. +// +// This function will return an error if the string input is not base64-encoded. +// +// base64.decode() -> +// +// Examples: +// +// base64.decode('aGVsbG8=') // return b'hello' +// base64.decode('aGVsbG8') // return b'hello' +// +// # Base64.DecodeUrl +// +// Introduced at version: 2 +// +// Decodes a base64url-encoded string to bytes. +// +// This function will return an error if the string input is not base64url-encoded. +// +// base64.decodeUrl() -> +// +// Examples: +// +// base64.decodeUrl('aGVsbG8=') // return b'hello' +// base64.decodeUrl('aGVsbG8') // return b'hello' +// base64.decodeUrl('____') // return b'\xff\xff\xff' +// +// # Base64.Encode +// +// Encodes bytes to a base64-encoded string. +// +// base64.encode() -> +// +// Examples: +// +// base64.encode(b'hello') // return b'aGVsbG8=' +// +// # Base64.EncodeUrl +// +// Introduced at version: 2 +// +// Encodes bytes to a base64url-encoded string. +// +// base64.encodeUrl() -> +// +// Examples: +// +// base64.encodeUrl(b'hello') // return 'aGVsbG8=' +// base64.encodeUrl(b'\xff\xff\xff') // return '____' +// +// # JSON.Encode +// +// Introduced at version: 1 +// +// Encodes a CEL value to a JSON string. +// +// json.encode() -> +// +// Examples: +// +// json.encode({'hello': 'world'}) // return '{"hello":"world"}' +// +// # JSON.Parse +// +// Introduced at version: 2 +// +// Parses a JSON string to a CEL value or a specific type. +// +// json.parse() -> +// json.parse(, ) -> +// +// Examples: +// +// json.parse('{"hello":"world"}') // return optional.of({'hello': 'world'}) +// json.parse('123', int) // return optional.of(123) +// +// # YAML.Encode +// +// Introduced at version: 2 +// +// Encodes a CEL value to a YAML string. +// +// yaml.encode() -> +// +// Examples: +// +// yaml.encode({'hello': 'world'}) // return 'hello: world\n' +// +// # YAML.Parse +// +// Introduced at version: 2 +// +// Parses a YAML string to a CEL value or a specific type. +// +// yaml.parse() -> +// yaml.parse(, ) -> +// +// Examples: +// +// yaml.parse('hello: world') // return optional.of({'hello': 'world'}) +// yaml.parse('123', int) // return optional.of(123) +func Encoders(options ...EncodersOption) cel.EnvOption { + l := &encoderLib{version: math.MaxUint32} + for _, o := range options { + l = o(l) + } + return cel.Lib(l) +} + +// EncodersOption declares a functional operator for configuring encoder extensions. +type EncodersOption func(*encoderLib) *encoderLib + +// EncodersVersion sets the library version for encoder extensions. +func EncodersVersion(version uint32) EncodersOption { + return func(lib *encoderLib) *encoderLib { + lib.version = version + return lib + } +} + +type encoderLib struct { + version uint32 +} + +func (*encoderLib) LibraryName() string { + return "cel.lib.ext.encoders" +} + +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 { + s := str.(types.String) + return bytesOrError(base64DecodeString(string(s))) + }))), + cel.Function("base64.encode", + cel.Overload("base64_encode_bytes", []*cel.Type{cel.BytesType}, cel.StringType, + cel.UnaryBinding(func(bytes ref.Val) ref.Val { + b := bytes.(types.Bytes) + return stringOrError(base64EncodeBytes([]byte(b))) + }))), + } + if lib.version >= 1 { + opts = append(opts, + cel.CostModel( + base64DecodeCostModel("base64_decode_string"), + base64EncodeCostModel("base64_encode_bytes"), + unboundedCostModel("json_encode_dyn"), + ), + 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)) + }))), + ) + } + if lib.version >= 2 { + var adapt types.Adapter = types.DefaultTypeAdapter + var prov types.Provider + opts = append(opts, + cel.CostModel( + base64DecodeCostModel("base64_decode_url_string"), + base64EncodeCostModel("base64_encode_url_bytes"), + unboundedCostModel("json_parse_string"), + unboundedCostModel("json_parse_string_type"), + unboundedCostModel("yaml_encode_dyn"), + unboundedCostModel("yaml_parse_string"), + unboundedCostModel("yaml_parse_string_type"), + ), + func(e *cel.Env) (*cel.Env, error) { + adapt = e.CELTypeAdapter() + prov = e.CELTypeProvider() + return e, nil + }, + cel.Function("base64.decodeUrl", + cel.Overload("base64_decode_url_string", []*cel.Type{cel.StringType}, cel.BytesType, + cel.UnaryBinding(func(str ref.Val) ref.Val { + s := str.(types.String) + return bytesOrError(base64DecodeURLString(string(s))) + }))), + cel.Function("base64.encodeUrl", + cel.Overload("base64_encode_url_bytes", []*cel.Type{cel.BytesType}, cel.StringType, + cel.UnaryBinding(func(bytes ref.Val) ref.Val { + b := bytes.(types.Bytes) + return stringOrError(base64EncodeURLBytes([]byte(b))) + }))), + cel.Function("json.parse", + cel.Overload("json_parse_string", + []*cel.Type{cel.StringType}, + cel.OptionalType(cel.DynType), + cel.UnaryBinding(func(val ref.Val) ref.Val { + str := val.(types.String) + return jsonParseString(adapt, string(str)) + }), + ), + cel.Overload("json_parse_string_type", + []*cel.Type{cel.StringType, types.NewTypeTypeWithParam(cel.TypeParamType("T"))}, + cel.OptionalType(cel.TypeParamType("T")), + cel.BinaryBinding(func(strVal, typeVal ref.Val) ref.Val { + str := strVal.(types.String) + targetType := typeVal.(ref.Type) + return jsonParseWithType(adapt, prov, string(str), targetType) + }), + ), + ), + cel.Function("yaml.encode", + cel.Overload("yaml_encode_dyn", []*cel.Type{cel.DynType}, cel.StringType, + cel.UnaryBinding(func(val ref.Val) ref.Val { + return stringOrError(yamlEncodeValue(val)) + }))), + cel.Function("yaml.parse", + cel.Overload("yaml_parse_string", + []*cel.Type{cel.StringType}, + cel.OptionalType(cel.DynType), + cel.UnaryBinding(func(val ref.Val) ref.Val { + str := val.(types.String) + return yamlParseString(adapt, string(str)) + }), + ), + cel.Overload("yaml_parse_string_type", + []*cel.Type{cel.StringType, types.NewTypeTypeWithParam(cel.TypeParamType("T"))}, + cel.OptionalType(cel.TypeParamType("T")), + cel.BinaryBinding(func(strVal, typeVal ref.Val) ref.Val { + str := strVal.(types.String) + targetType := typeVal.(ref.Type) + return yamlParseWithType(adapt, prov, string(str), targetType) + }), + ), + ), + ) + } + return opts +} + +func (lib *encoderLib) ProgramOptions() []cel.ProgramOption { + return nil +} + +// YAMLEncode encodes a CEL ref.Val to its YAML string representation. +func YAMLEncode(val ref.Val) (string, error) { + return yamlEncodeValue(val) +} + +// YAMLParse parses a YAML string into a dynamic CEL ref.Val using the given type adapter. +// If adapter is nil, types.DefaultTypeAdapter is used. +func YAMLParse(adapter types.Adapter, str string) (ref.Val, error) { + if adapter == nil { + adapter = types.DefaultTypeAdapter + } + if len(str) > maxYAMLSize { + return nil, fmt.Errorf("yaml parse error: string size exceeds maximum allowed limit of %d bytes", maxYAMLSize) + } + jsonStr, ok := yamlToJSON(str) + if !ok { + return nil, fmt.Errorf("yaml parse error: invalid YAML input") + } + return JSONParse(adapter, jsonStr) +} + +// YAMLParseWithType parses a YAML string into a typed CEL ref.Val conforming to targetType. +// If adapter is nil, types.DefaultTypeAdapter is used. If provider implements types.Adapter, it will be used as the adapter. +func YAMLParseWithType(adapter types.Adapter, provider types.Provider, str string, targetType ref.Type) (ref.Val, error) { + if adapter == nil { + adapter = types.DefaultTypeAdapter + } + if provAdapter, ok := provider.(types.Adapter); ok && provAdapter != nil { + adapter = provAdapter + } + if len(str) > maxYAMLSize { + return nil, fmt.Errorf("yaml parse error: string size exceeds maximum allowed limit of %d bytes", maxYAMLSize) + } + jsonStr, ok := yamlToJSON(str) + if !ok { + return nil, fmt.Errorf("yaml parse error: failed to parse YAML to type %v", targetType) + } + return JSONParseWithType(adapter, provider, jsonStr, targetType) +} + +// JSONEncode encodes a CEL ref.Val to its JSON string representation. +func JSONEncode(val ref.Val) (string, error) { + return jsonEncodeValue(val) +} + +// JSONParse parses a JSON string into a dynamic CEL ref.Val using the given type adapter. +// If adapter is nil, types.DefaultTypeAdapter is used. +func JSONParse(adapter types.Adapter, str string) (ref.Val, error) { + if adapter == nil { + adapter = types.DefaultTypeAdapter + } + if len(str) > maxJSONSize { + return nil, fmt.Errorf("json parse error: string size exceeds maximum allowed limit of %d bytes", maxJSONSize) + } + obj, ok := jsonParseDynamic(str) + if !ok { + return nil, fmt.Errorf("json parse error: invalid JSON input") + } + return adapter.NativeToValue(obj), nil +} + +// JSONParseWithType parses a JSON string into a typed CEL ref.Val conforming to targetType. +// If adapter is nil, types.DefaultTypeAdapter is used. If provider implements types.Adapter, it will be used as the adapter. +func JSONParseWithType(adapter types.Adapter, provider types.Provider, str string, targetType ref.Type) (ref.Val, error) { + if adapter == nil { + adapter = types.DefaultTypeAdapter + } + if provAdapter, ok := provider.(types.Adapter); ok && provAdapter != nil { + adapter = provAdapter + } + if len(str) > maxJSONSize { + return nil, fmt.Errorf("json parse error: string size exceeds maximum allowed limit of %d bytes", maxJSONSize) + } + val, ok := jsonParseValue(adapter, provider, str, targetType) + if !ok { + return nil, fmt.Errorf("json parse error: failed to parse JSON to type %v", targetType) + } + return adapter.NativeToValue(val), nil +} + +// Base64Encode encodes a byte slice into a standard base64 string. +func Base64Encode(bytes []byte) string { + return base64.StdEncoding.EncodeToString(bytes) +} + +// Base64Decode decodes a base64 encoded string into a byte slice. +func Base64Decode(str string) ([]byte, error) { + return base64DecodeString(str) +} + +func base64DecodeString(str string) ([]byte, error) { + b, err := base64.StdEncoding.DecodeString(str) + if err == nil { + return b, nil + } + return base64.RawStdEncoding.DecodeString(str) +} + +func base64DecodeURLString(str string) ([]byte, error) { + b, err := base64.URLEncoding.DecodeString(str) + if err == nil { + return b, nil + } + if _, tryAltEncoding := err.(base64.CorruptInputError); tryAltEncoding { + return base64.RawURLEncoding.DecodeString(str) + } + return nil, err +} + +func base64EncodeBytes(bytes []byte) (string, error) { + return base64.StdEncoding.EncodeToString(bytes), nil +} + +func base64EncodeURLBytes(bytes []byte) (string, error) { + return base64.URLEncoding.EncodeToString(bytes), nil +} + +func estimateEncode(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) != 1 { + return nil + } + sz := cost.EstimateSize(estimator, args[0]) + estimate := sz.MultiplyByCostFactor(cost.StringCostFactor).Add(cost.CallCostEstimate) + resSize := estimateEncodeSize(sz) + return &cost.CallEstimate{CostEstimate: estimate, ResultSize: &resSize} +} + +func estimateDecode(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) != 1 { + return nil + } + sz := cost.EstimateSize(estimator, args[0]) + estimate := sz.MultiplyByCostFactor(cost.StringCostFactor).Add(cost.CallCostEstimate) + resSize := estimateDecodeSize(sz) + return &cost.CallEstimate{CostEstimate: estimate, ResultSize: &resSize} +} + +func estimateEncodeSize(sz cost.SizeEstimate) cost.SizeEstimate { + minVal := (sz.Min*4 + 2) / 3 + maxVal := (sz.Max*4 + 2) / 3 + if sz.Max > math.MaxUint64/4 { + maxVal = math.MaxUint64 + } + return cost.SizeEstimate{Min: minVal, Max: maxVal} +} + +func estimateDecodeSize(sz cost.SizeEstimate) cost.SizeEstimate { + minVal := sz.Min * 3 / 4 + maxVal := sz.Max * 3 / 4 + return cost.SizeEstimate{Min: minVal, Max: maxVal} +} + +func jsonEncodeValue(val ref.Val) (string, error) { + switch v := val.(type) { + case types.Bool: + if v { + return "true", nil + } + return "false", nil + case types.Null: + return "null", nil + case types.Int: + if v >= -9007199254740991 && v <= 9007199254740991 { + return strconv.FormatInt(int64(v), 10), nil + } + return strconv.Quote(strconv.FormatInt(int64(v), 10)), nil + case types.Uint: + if v <= 9007199254740991 { + return strconv.FormatUint(uint64(v), 10), nil + } + return strconv.Quote(strconv.FormatUint(uint64(v), 10)), nil + case types.Double: + f := float64(v) + if math.IsNaN(f) || math.IsInf(f, 0) { + return "", fmt.Errorf("json encode error: NaN and Inf are not supported in JSON") + } + b, _ := json.Marshal(f) + return string(b), nil + case types.String: + str := string(v) + if len(str) < 128 && !strings.ContainsAny(str, "\"\\\n\r\t\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f<>&") { + return strconv.Quote(str), nil + } + b, _ := json.Marshal(str) + return string(b), nil + case types.Bytes: + return strconv.Quote(base64.StdEncoding.EncodeToString([]byte(v))), nil + } + if msg, ok := getProtoMessage(val); ok { + jsonBytes, err := protojson.Marshal(msg) + if err != nil { + return "", err + } + return string(jsonBytes), nil + } + if val.Type().HasTrait(traits.FieldTesterType) { + if encoded, ok := jsonEncodeNativeStruct(val.Value()); ok { + return encoded, nil + } + } + if lister, ok := val.(traits.Lister); ok { + return jsonEncodeList(lister) + } + if mapper, ok := val.(traits.Mapper); ok { + return jsonEncodeMap(mapper) + } + 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 +} + +func getProtoMessage(val ref.Val) (proto.Message, bool) { + if msg, ok := val.(proto.Message); ok && msg != nil { + return msg, true + } + if msg, ok := val.Value().(proto.Message); ok && msg != nil { + return msg, true + } + return nil, false +} + +func jsonEncodeNativeStruct(nativeVal any) (string, bool) { + if nativeVal == nil { + return "", false + } + if _, isProto := nativeVal.(proto.Message); isProto { + return "", false + } + rt := reflect.TypeOf(nativeVal) + if rt.Kind() == reflect.Pointer { + rt = rt.Elem() + } + if rt.Kind() == reflect.Struct { + jsonBytes, err := json.Marshal(nativeVal) + if err == nil { + return string(jsonBytes), true + } + } + return "", false +} + +func jsonEncodeList(lister traits.Lister) (string, error) { + szVal := lister.Size() + szInt, ok := szVal.(types.Int) + if !ok { + return "", fmt.Errorf("invalid list size: %v", szVal) + } + sz := int(szInt) + if sz == 0 { + return "[]", nil + } + var sb strings.Builder + sb.WriteByte('[') + for i := 0; i < sz; i++ { + if i > 0 { + sb.WriteByte(',') + } + elem := lister.Get(types.Int(i)) + elemStr, err := jsonEncodeValue(elem) + if err != nil { + return "", err + } + sb.WriteString(elemStr) + } + sb.WriteByte(']') + return sb.String(), nil +} + +func jsonEncodeMap(mapper traits.Mapper) (string, error) { + szVal := mapper.Size() + szInt, ok := szVal.(types.Int) + if !ok { + return "", fmt.Errorf("invalid map size: %v", szVal) + } + sz := int(szInt) + if sz == 0 { + return "{}", nil + } + var sb strings.Builder + sb.WriteByte('{') + it := mapper.Iterator() + first := true + for it.HasNext() == types.True { + k := it.Next() + kStr, ok := k.(types.String) + if !ok { + kStrVal, err := jsonEncodeValue(k) + if err != nil { + return "", err + } + kStr = types.String(kStrVal) + } + kQuoted, _ := json.Marshal(string(kStr)) + v := mapper.Get(k) + vStr, err := jsonEncodeValue(v) + if err != nil { + return "", err + } + if !first { + sb.WriteByte(',') + } + first = false + sb.Write(kQuoted) + sb.WriteByte(':') + sb.WriteString(vStr) + } + sb.WriteByte('}') + return sb.String(), nil +} + +func jsonUnmarshalExact(str string, out any) error { + dec := json.NewDecoder(strings.NewReader(str)) + if err := dec.Decode(out); err != nil { + return err + } + if _, err := dec.Token(); err != io.EOF { + return fmt.Errorf("unexpected trailing data") + } + return nil +} + +func jsonParsePrimitive[T any](str string, convert func(any) (T, bool)) (T, bool) { + dec := json.NewDecoder(strings.NewReader(str)) + dec.UseNumber() + tok, err := dec.Token() + if err != nil { + var zero T + return zero, false + } + val, ok := convert(tok) + if !ok { + var zero T + return zero, false + } + if _, err := dec.Token(); err != io.EOF { + var zero T + return zero, false + } + return val, true +} + +func jsonParseDynamic(str string) (any, bool) { + trimmed := strings.TrimSpace(str) + if len(trimmed) == 0 { + return nil, false + } + if len(trimmed) >= 2 && trimmed[0] == '"' && trimmed[len(trimmed)-1] == '"' && !strings.ContainsRune(trimmed, '\\') { + return trimmed[1 : len(trimmed)-1], true + } + if trimmed == "true" { + return true, true + } + if trimmed == "false" { + return false, true + } + if trimmed == "null" { + return types.NullValue, true + } + if len(trimmed) > 0 && (trimmed[0] == '-' || (trimmed[0] >= '0' && trimmed[0] <= '9')) { + if !strings.ContainsAny(trimmed, " \t\r\n,[]{}:") { + if _, err := strconv.ParseInt(trimmed, 10, 64); err == nil { + return json.Number(trimmed), true + } + if _, err := strconv.ParseUint(trimmed, 10, 64); err == nil { + return json.Number(trimmed), true + } + if f, err := strconv.ParseFloat(trimmed, 64); err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) { + return json.Number(trimmed), true + } + } + } + dec := json.NewDecoder(strings.NewReader(str)) + dec.UseNumber() + var obj any + if err := dec.Decode(&obj); err != nil { + return nil, false + } + if _, err := dec.Token(); err != io.EOF { + return nil, false + } + return obj, true +} + +func jsonParseString(adapter types.Adapter, str string) ref.Val { + if len(str) > maxJSONSize { + return types.NewErr("json parse error: string size exceeds maximum allowed limit of %d bytes", maxJSONSize) + } + obj, ok := jsonParseDynamic(str) + if !ok { + return types.OptionalNone + } + return types.OptionalOf(adapter.NativeToValue(obj)) +} + +func jsonParseWithType(adapter types.Adapter, provider types.Provider, str string, targetType ref.Type) ref.Val { + if adapter == nil { + adapter = types.DefaultTypeAdapter + } + if provAdapter, ok := provider.(types.Adapter); ok && provAdapter != nil { + adapter = provAdapter + } + if len(str) > maxJSONSize { + return types.NewErr("json parse error: string size exceeds maximum allowed limit of %d bytes", maxJSONSize) + } + val, ok := jsonParseValue(adapter, provider, str, targetType) + if !ok { + return types.OptionalNone + } + return types.OptionalOf(adapter.NativeToValue(val)) +} + +func jsonParseValue(adapter types.Adapter, provider types.Provider, str string, targetType ref.Type) (any, bool) { + switch t := targetType.(type) { + case *types.Type: + switch t.Kind() { + case types.DynKind, types.AnyKind: + return jsonParseDynamic(str) + case types.StringKind: + trimmed := strings.TrimSpace(str) + if len(trimmed) >= 2 && trimmed[0] == '"' && trimmed[len(trimmed)-1] == '"' && !strings.ContainsRune(trimmed, '\\') { + return trimmed[1 : len(trimmed)-1], true + } + return jsonParsePrimitive(str, func(t any) (any, bool) { s, ok := t.(string); return s, ok }) + case types.BoolKind: + trimmed := strings.TrimSpace(str) + if trimmed == "true" { + return true, true + } + if trimmed == "false" { + return false, true + } + return false, false + case types.IntKind: + trimmed := strings.TrimSpace(str) + if i, err := strconv.ParseInt(trimmed, 10, 64); err == nil { + return i, true + } + return false, false + case types.UintKind: + trimmed := strings.TrimSpace(str) + if u, err := strconv.ParseUint(trimmed, 10, 64); err == nil { + return u, true + } + return false, false + case types.DoubleKind: + trimmed := strings.TrimSpace(str) + if len(trimmed) > 0 && (trimmed[0] == '-' || (trimmed[0] >= '0' && trimmed[0] <= '9')) { + if f, err := strconv.ParseFloat(trimmed, 64); err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) { + return f, true + } + } + return false, false + case types.BytesKind: + return jsonParsePrimitive(str, func(t any) (any, bool) { + s, ok := t.(string) + if !ok { + return nil, false + } + b, err := base64DecodeString(s) + return b, err == nil + }) + case types.NullTypeKind: + trimmed := strings.TrimSpace(str) + if trimmed == "null" { + return types.NullValue, true + } + return false, false + case types.TimestampKind: + return jsonParsePrimitive(str, func(t any) (any, bool) { + s, ok := t.(string) + if !ok { + return nil, false + } + ts, err := types.ParseTimestamp(s) + return ts, err == nil + }) + case types.DurationKind: + return jsonParsePrimitive(str, func(t any) (any, bool) { + s, ok := t.(string) + if !ok { + return nil, false + } + dur, err := time.ParseDuration(s) + return dur, err == nil + }) + case types.ListKind: + elemType := types.DynType + if len(t.Parameters()) > 0 { + elemType = t.Parameters()[0] + } + return jsonParseTypedList(adapter, provider, str, elemType) + case types.MapKind: + elemType := types.DynType + if len(t.Parameters()) >= 2 { + elemType = t.Parameters()[1] + } + return jsonParseTypedMap(adapter, provider, str, elemType) + case types.StructKind: + return jsonParseStructNative(provider, str, t.TypeName()) + } + default: + if st, ok := targetType.(types.StructTypeDescriptor); ok { + if rt := st.ReflectType(); rt != nil { + return jsonUnmarshalNative(str, rt) + } + } + return jsonParseStructNative(provider, str, targetType.TypeName()) + } + return nil, false +} + +func jsonParseTypedList(adapter types.Adapter, provider types.Provider, str string, elemType *types.Type) (any, bool) { + switch elemType.Kind() { + case types.StringKind: + var l []string + if jsonUnmarshalExact(str, &l) == nil { + return l, true + } + return nil, false + case types.IntKind: + var l []int64 + if jsonUnmarshalExact(str, &l) == nil { + return l, true + } + return nil, false + case types.UintKind: + var l []uint64 + if jsonUnmarshalExact(str, &l) == nil { + return l, true + } + return nil, false + case types.DoubleKind: + var l []float64 + if jsonUnmarshalExact(str, &l) == nil { + return l, true + } + return nil, false + case types.BoolKind: + var l []bool + if jsonUnmarshalExact(str, &l) == nil { + return l, true + } + return nil, false + case types.DynKind, types.AnyKind: + var l []any + if jsonUnmarshalExact(str, &l) == nil { + return l, true + } + return nil, false + } + var rawList []json.RawMessage + if jsonUnmarshalExact(str, &rawList) != nil { + return nil, false + } + elements := make([]any, len(rawList)) + for i, rawElem := range rawList { + elem, ok := jsonParseValue(adapter, provider, string(rawElem), elemType) + if !ok { + return nil, false + } + elements[i] = elem + } + return elements, true +} + +func jsonParseTypedMap(adapter types.Adapter, provider types.Provider, str string, valType *types.Type) (any, bool) { + switch valType.Kind() { + case types.StringKind: + var m map[string]string + if jsonUnmarshalExact(str, &m) == nil { + return m, true + } + return nil, false + case types.IntKind: + var m map[string]int64 + if jsonUnmarshalExact(str, &m) == nil { + return m, true + } + return nil, false + case types.UintKind: + var m map[string]uint64 + if jsonUnmarshalExact(str, &m) == nil { + return m, true + } + return nil, false + case types.DoubleKind: + var m map[string]float64 + if jsonUnmarshalExact(str, &m) == nil { + return m, true + } + return nil, false + case types.BoolKind: + var m map[string]bool + if jsonUnmarshalExact(str, &m) == nil { + return m, true + } + return nil, false + case types.DynKind, types.AnyKind: + var m map[string]any + if jsonUnmarshalExact(str, &m) == nil { + return m, true + } + return nil, false + } + var rawMap map[string]json.RawMessage + if jsonUnmarshalExact(str, &rawMap) != nil { + return nil, false + } + entries := make(map[string]any, len(rawMap)) + for k, rawVal := range rawMap { + val, ok := jsonParseValue(adapter, provider, string(rawVal), valType) + if !ok { + return nil, false + } + entries[k] = val + } + return entries, true +} + +func jsonUnmarshalNative(str string, rt reflect.Type) (any, bool) { + if rt.Kind() == reflect.Pointer { + rt = rt.Elem() + } + if rt.Kind() == reflect.Struct { + ptr := reflect.New(rt) + if jsonUnmarshalExact(str, ptr.Interface()) != nil { + return nil, false + } + return ptr.Interface(), true + } + return nil, false +} + +func jsonUnmarshalProto(str string, template proto.Message) (proto.Message, bool) { + newMsg := template.ProtoReflect().New().Interface() + if err := protojson.Unmarshal([]byte(str), newMsg); err != nil { + return nil, false + } + return newMsg, true +} + +type structPrototype struct { + protoMsg proto.Message + refType reflect.Type +} + +var ( + structPrototypeCache sync.Map // map[string]structPrototype +) + +func jsonParseStructNative(provider types.Provider, str string, typeName string) (any, bool) { + switch typeName { + case "google.protobuf.Value": + return jsonUnmarshalProto(str, &structpb.Value{}) + case "google.protobuf.Struct": + return jsonUnmarshalProto(str, &structpb.Struct{}) + case "google.protobuf.ListValue": + return jsonUnmarshalProto(str, &structpb.ListValue{}) + } + if protoVal, ok := structPrototypeCache.Load(typeName); ok { + p := protoVal.(structPrototype) + if p.protoMsg != nil { + return jsonUnmarshalProto(str, p.protoMsg) + } + if p.refType != nil { + return jsonUnmarshalNative(str, p.refType) + } + } + if provider != nil { + zeroVal := provider.NewValue(typeName, map[string]ref.Val{}) + if types.IsError(zeroVal) { + return nil, false + } + nativeVal := zeroVal.Value() + if nativeVal == nil { + return nil, false + } + if msg, ok := nativeVal.(proto.Message); ok { + structPrototypeCache.Store(typeName, structPrototype{protoMsg: msg}) + return jsonUnmarshalProto(str, msg) + } + rt := reflect.TypeOf(nativeVal) + if rt.Kind() == reflect.Pointer { + rt = rt.Elem() + } + structPrototypeCache.Store(typeName, structPrototype{refType: rt}) + return jsonUnmarshalNative(str, rt) + } + return nil, false +} + +func yamlEncodeValue(val ref.Val) (string, error) { + jsonStr, err := jsonEncodeValue(val) + if err != nil { + return "", err + } + dec := json.NewDecoder(strings.NewReader(jsonStr)) + dec.UseNumber() + var obj any + if err := dec.Decode(&obj); err != nil { + return "", err + } + cleaned := normalizeJSONNumber(obj) + yamlBytes, err := yaml.Marshal(cleaned) + if err != nil { + return "", err + } + return string(yamlBytes), nil +} + +func yamlToJSON(str string) (string, bool) { + trimmed := strings.TrimSpace(str) + if len(trimmed) == 0 { + return "", false + } + dec := yaml.NewDecoder(strings.NewReader(str)) + var node yaml.Node + if err := dec.Decode(&node); err != nil { + return "", false + } + if len(node.Content) == 0 { + return "", false + } + var extra yaml.Node + if err := dec.Decode(&extra); err != io.EOF { + return "", false + } + var obj any + if err := node.Decode(&obj); err != nil { + return "", false + } + cleaned := cleanYAMLObj(obj) + jsonBytes, err := json.Marshal(cleaned) + if err != nil { + return "", false + } + return string(jsonBytes), true +} + +func yamlParseString(adapter types.Adapter, str string) ref.Val { + if len(str) > maxYAMLSize { + return types.NewErr("yaml parse error: string size exceeds maximum allowed limit of %d bytes", maxYAMLSize) + } + jsonStr, ok := yamlToJSON(str) + if !ok { + return types.OptionalNone + } + return jsonParseString(adapter, jsonStr) +} + +func yamlParseWithType(adapter types.Adapter, provider types.Provider, str string, targetType ref.Type) ref.Val { + if len(str) > maxYAMLSize { + return types.NewErr("yaml parse error: string size exceeds maximum allowed limit of %d bytes", maxYAMLSize) + } + jsonStr, ok := yamlToJSON(str) + if !ok { + return types.OptionalNone + } + return jsonParseWithType(adapter, provider, jsonStr, targetType) +} + +func cleanYAMLObj(val any) any { + switch v := val.(type) { + case map[string]any: + m := make(map[string]any, len(v)) + for k, elem := range v { + m[k] = cleanYAMLObj(elem) + } + return m + case map[any]any: + m := make(map[string]any, len(v)) + for k, elem := range v { + m[fmt.Sprint(k)] = cleanYAMLObj(elem) + } + return m + case []any: + l := make([]any, len(v)) + for i, elem := range v { + l[i] = cleanYAMLObj(elem) + } + return l + case time.Time: + return v.Format(time.RFC3339Nano) + default: + return val + } +} + +func normalizeJSONNumber(val any) any { + switch v := val.(type) { + case json.Number: + if i, err := v.Int64(); err == nil { + return i + } + if u, err := strconv.ParseUint(string(v), 10, 64); err == nil { + return u + } + if f, err := v.Float64(); err == nil { + return f + } + return string(v) + case map[string]any: + m := make(map[string]any, len(v)) + for k, elem := range v { + m[k] = normalizeJSONNumber(elem) + } + return m + case []any: + l := make([]any, len(v)) + for i, elem := range v { + l[i] = normalizeJSONNumber(elem) + } + return l + default: + return val + } +} + +func base64EncodeCostModel(id string) cost.OverloadModel { + scaled := cost.Scale(cost.Length(cost.Arg(0)), 4.0/3) + return cost.Overload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.StringScan(cost.Length(cost.Arg(0))))), + // base64 of n bytes is 4 * ceil(n / 3) characters, between 4n/3 and 4n/3 + 3. + cost.ResultSize(cost.Ranged(scaled, cost.Sum(scaled, cost.Const(3)))), + cost.BeforeVersion(1, cost.ManualEstimator(estimateEncode)), + ) +} + +func base64DecodeCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.StringScan(cost.Length(cost.Arg(0))))), + // Decoding n characters yields at most 3n/4 bytes. It yields fewer for padding and for the + // carriage returns and newlines the decoder skips, so there is no lower bound. A bytes value + // has size at least 1 under the aggregate strategy. + cost.ResultSize(cost.AtMost(cost.AtLeastOneQuantity(cost.Scale(cost.Length(cost.Arg(0)), 0.75)))), + cost.BeforeVersion(1, cost.ManualEstimator(estimateDecode)), + ) +} + +// unboundedCostModel charges the maximum cost and reports an unbounded result size for JSON and +// YAML functions: encoding traverses an arbitrary container tree without inspecting it through the +// cost tracker, and parsing/decoding (especially with YAML anchors and aliases) can construct an +// arbitrarily deeply nested or large value graph not bounded by the input length. +func unboundedCostModel(id string) cost.OverloadModel { + unbounded := cost.AtMost(cost.Const(math.MaxUint64)) + return cost.Overload(id, cost.EvalCost(unbounded), cost.ResultSize(unbounded)) +} diff --git a/vendor/cel.dev/cel-go/ext/extension_option_factory.go b/vendor/cel.dev/cel-go/ext/extension_option_factory.go new file mode 100644 index 00000000000..8af0d3ed4a4 --- /dev/null +++ b/vendor/cel.dev/cel-go/ext/extension_option_factory.go @@ -0,0 +1,111 @@ +// 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 ext + +import ( + "fmt" + "sync" + + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/env" +) + +// ExtensionOptionFactory converts an ExtensionConfig value to a CEL environment option. +func ExtensionOptionFactory(configElement any) (cel.EnvOption, bool) { + ext, isExtension := configElement.(*env.Extension) + if !isExtension { + return nil, false + } + name := ext.Name + alias, found := extAliases()[name] + if found { + name = alias + } + fac, found := extFactories()[name] + if !found { + return nil, false + } + // If the version is 'latest', set the version value to the max uint. + ver, err := ext.VersionNumber() + if err != nil { + return func(*cel.Env) (*cel.Env, error) { + return nil, fmt.Errorf("invalid extension version: %s - %s", ext.Name, ext.Version) + }, true + } + return fac(ver), true +} + +// extensionFactory accepts a version and produces a CEL environment associated with the versioned extension. +type extensionFactory func(uint32) cel.EnvOption + +var ( + extFactoriesOnce sync.Once + extFactoriesMap map[string]extensionFactory + extAliasesMap map[string]string +) + +func extFactories() map[string]extensionFactory { + extFactoriesOnce.Do(initExtFactories) + return extFactoriesMap +} + +func extAliases() map[string]string { + extFactoriesOnce.Do(initExtFactories) + return extAliasesMap +} + +func initExtFactories() { + extFactoriesMap = map[string]extensionFactory{ + "cel.lib.ext.cel.bindings": func(version uint32) cel.EnvOption { + return Bindings(BindingsVersion(version)) + }, + "cel.lib.ext.encoders": func(version uint32) cel.EnvOption { + return Encoders(EncodersVersion(version)) + }, + "cel.lib.ext.lists": func(version uint32) cel.EnvOption { + return Lists(ListsVersion(version)) + }, + "cel.lib.ext.math": func(version uint32) cel.EnvOption { + return Math(MathVersion(version)) + }, + "cel.lib.ext.protos": func(version uint32) cel.EnvOption { + return Protos(ProtosVersion(version)) + }, + "cel.lib.ext.sets": func(version uint32) cel.EnvOption { + return Sets(SetsVersion(version)) + }, + "cel.lib.ext.strings": func(version uint32) cel.EnvOption { + return Strings(StringsVersion(version)) + }, + "cel.lib.ext.comprev2": func(version uint32) cel.EnvOption { + return TwoVarComprehensions(TwoVarComprehensionsVersion(version)) + }, + "cel.lib.ext.regex": func(version uint32) cel.EnvOption { + return Regex(RegexVersion(version)) + }, + } + + extAliasesMap = map[string]string{ + "bindings": "cel.lib.ext.cel.bindings", + "encoders": "cel.lib.ext.encoders", + "lists": "cel.lib.ext.lists", + "math": "cel.lib.ext.math", + "protos": "cel.lib.ext.protos", + "sets": "cel.lib.ext.sets", + "strings": "cel.lib.ext.strings", + "two-var-comprehensions": "cel.lib.ext.comprev2", + "regex": "cel.lib.ext.regex", + } +} diff --git a/vendor/github.com/google/cel-go/ext/formatting.go b/vendor/cel.dev/cel-go/ext/formatting.go similarity index 99% rename from vendor/github.com/google/cel-go/ext/formatting.go rename to vendor/cel.dev/cel-go/ext/formatting.go index 35fb17048ef..f8633dbc082 100644 --- a/vendor/github.com/google/cel-go/ext/formatting.go +++ b/vendor/cel.dev/cel-go/ext/formatting.go @@ -26,12 +26,12 @@ import ( "golang.org/x/text/language" "golang.org/x/text/message" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) type clauseImpl func(ref.Val, string) (string, error) diff --git a/vendor/github.com/google/cel-go/ext/formatting_v2.go b/vendor/cel.dev/cel-go/ext/formatting_v2.go similarity index 99% rename from vendor/github.com/google/cel-go/ext/formatting_v2.go rename to vendor/cel.dev/cel-go/ext/formatting_v2.go index f923cc7e103..969a779e108 100644 --- a/vendor/github.com/google/cel-go/ext/formatting_v2.go +++ b/vendor/cel.dev/cel-go/ext/formatting_v2.go @@ -24,11 +24,11 @@ import ( "time" "unicode" - "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" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) type clauseImplV2 func(ref.Val) (string, error) diff --git a/vendor/github.com/google/cel-go/ext/guards.go b/vendor/cel.dev/cel-go/ext/guards.go similarity index 92% rename from vendor/github.com/google/cel-go/ext/guards.go rename to vendor/cel.dev/cel-go/ext/guards.go index 1461c0416d7..83e64e89a0d 100644 --- a/vendor/github.com/google/cel-go/ext/guards.go +++ b/vendor/cel.dev/cel-go/ext/guards.go @@ -15,9 +15,9 @@ package ext import ( - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // function invocation guards for common call signatures within extension functions. diff --git a/vendor/cel.dev/cel-go/ext/lists.go b/vendor/cel.dev/cel-go/ext/lists.go new file mode 100644 index 00000000000..cd679b9663b --- /dev/null +++ b/vendor/cel.dev/cel-go/ext/lists.go @@ -0,0 +1,1339 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ext + +import ( + "fmt" + "math" + "slices" + "sort" + + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/decls" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" + "cel.dev/cel-go/parser" +) + +var comparableTypes = []*cel.Type{ + cel.IntType, + cel.UintType, + cel.DoubleType, + cel.BoolType, + cel.DurationType, + cel.TimestampType, + cel.StringType, + cel.BytesType, +} + +// Lists returns a cel.EnvOption to configure extended functions for list manipulation. +// As a general note, all indices are zero-based. +// +// # Distinct +// +// Introduced in version: 2 (cost support in version 3) +// +// Returns the distinct elements of a list. +// +// .distinct() -> +// +// Examples: +// +// [1, 2, 2, 3, 3, 3].distinct() // return [1, 2, 3] +// ["b", "b", "c", "a", "c"].distinct() // return ["b", "c", "a"] +// [1, "b", 2, "b"].distinct() // return [1, "b", 2] +// +// # Range +// +// Introduced in version: 2 (cost support in version 3) +// +// Returns a list of integers from 0 to n-1. +// +// lists.range() -> +// +// Examples: +// +// lists.range(5) -> [0, 1, 2, 3, 4] +// +// # Reverse +// +// Introduced in version: 2 (cost support in version 3) +// +// Returns the elements of a list in reverse order. +// +// .reverse() -> +// +// Examples: +// +// [5, 3, 1, 2].reverse() // return [2, 1, 3, 5] +// +// # Slice +// +// Introduced in version: 0 (cost support in version 3) +// +// Returns a new sub-list using the indexes provided. +// +// .slice(, ) -> +// +// Examples: +// +// [1,2,3,4].slice(1, 3) // return [2, 3] +// [1,2,3,4].slice(2, 4) // return [3 ,4] +// +// # Flatten +// +// Introduced in version: 1 (cost support in version 3) +// +// Flattens a list recursively. +// If an optional depth is provided, the list is flattened to a the specified level. +// A negative depth value will result in an error. +// +// .flatten() -> +// .flatten() -> +// +// Examples: +// +// [1,[2,3],[4]].flatten() // return [1, 2, 3, 4] +// [1,[2,[3,4]]].flatten() // return [1, 2, [3, 4]] +// [1,2,[],[],[3,4]].flatten() // return [1, 2, 3, 4] +// [1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]] +// [1,[2,[3,[4]]]].flatten(-1) // error +// +// # Sort +// +// Introduced in version: 2 (cost support in version 3) +// +// Sorts a list with comparable elements. If the element type is not comparable +// or the element types are not the same, the function will produce an error. +// +// .sort() -> +// T in {int, uint, double, bool, duration, timestamp, string, bytes} +// +// Examples: +// +// [3, 2, 1].sort() // return [1, 2, 3] +// ["b", "c", "a"].sort() // return ["a", "b", "c"] +// [1, "b"].sort() // error +// [[1, 2, 3]].sort() // error +// +// # SortBy +// +// Introduced in version: 2 (cost support in version 3) +// +// Sorts a list by a key value, i.e., the order is determined by the result of +// an expression applied to each element of the list. +// The output of the key expression must be a comparable type, otherwise the +// function will return an error. +// +// .sortBy(, ) -> +// keyExpr returns a value in {int, uint, double, bool, duration, timestamp, string, bytes} +// +// Examples: +// +// [ +// Player { name: "foo", score: 0 }, +// Player { name: "bar", score: -10 }, +// Player { name: "baz", score: 1000 }, +// ].sortBy(e, e.score).map(e, e.name) +// == ["bar", "foo", "baz"] +// +// # HasOnly +// +// Introduced in version: 5 +// +// Returns whether every element of the target list is contained within the +// argument list, i.e. whether the target is a subset of the argument. Standard +// CEL equality is used to determine whether two elements are equal, and neither +// list is required to contain unique elements. An empty target list always +// returns true. +// +// .hasOnly() -> +// +// Examples: +// +// [].hasOnly([1, 2]) // return true +// [1, 1, 2].hasOnly([1, 2, 3]) // return true +// [1, 2.0, 3u].hasOnly([1.0, 2u, 3]) // return true +// [1, 4].hasOnly([1, 2, 3]) // return false +// [1].hasOnly([]) // return false +// +// # HasAny +// +// Introduced in version: 5 +// +// Returns whether the target list has at least one element which is equal to an +// element of the argument list. If either list is empty, the result is false. +// +// .hasAny() -> +// +// Examples: +// +// [1, 2, 3].hasAny([3, 4]) // return true +// [[1], [2, 3]].hasAny([[1, 2], [2, 3.0]]) // return true +// [1, 2].hasAny([3, 4]) // return false +// [1].hasAny([]) // return false +// +// # HasAll +// +// Introduced in version: 5 +// +// Returns whether every element of the argument list is contained within the +// target list, i.e. whether the target is a superset of the argument. An empty +// argument list always returns true. +// +// .hasAll() -> +// +// Examples: +// +// [1, 2, 3].hasAll([]) // return true +// [1, 2, 3, 4].hasAll([2, 3]) // return true +// [1, 2.0, 3u].hasAll([1.0, 2u, 3]) // return true +// [1, 2, 3].hasAll([2, 4]) // return false +// +// # HasExactly +// +// Introduced in version: 5 +// +// Returns whether the target and argument lists are set equivalent, i.e. every +// element of the target list is equal to an element of the argument list and +// vice versa. The lists may differ in size since neither is guaranteed to hold +// unique elements, so size does not factor into the computation. +// +// .hasExactly() -> +// +// Examples: +// +// [].hasExactly([]) // return true +// [1].hasExactly([1, 1]) // return true +// [1].hasExactly([1u, 1.0]) // return true +// [1, 2, 3].hasExactly([3u, 2.0, 1]) // return true +// [1, 2].hasExactly([1, 2, 3]) // return false +func Lists(options ...ListsOption) cel.EnvOption { + 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 + maxRangeSize int64 +} + +// LibraryName implements the SingletonLibrary interface method. +func (listsLib) LibraryName() string { + return "cel.lib.ext.lists" +} + +// ListsOption is a functional interface for configuring the strings library. +type ListsOption func(*listsLib) *listsLib + +// ListsVersion configures the version of the string library. +// +// The version limits which functions are available. Only functions introduced +// below or equal to the given version included in the library. If this option +// is not set, all functions are available. +// +// See the library documentation to determine which version a function was introduced. +// If the documentation does not state which version a function was introduced, it can +// be assumed to be introduced at version 0, when the library was first created. +func ListsVersion(version uint32) ListsOption { + return func(lib *listsLib) *listsLib { + lib.version = version + return lib + } +} + +// ListsMaxRangeSize sets the maximum number of elements lists.range() will +// allocate. If not set, the default is 1,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")) + listListType := cel.ListType(listType) + listDyn := cel.ListType(cel.DynType) + var adapt func() types.Adapter + captureAdapt := func(e *cel.Env) (*cel.Env, error) { + adapt = e.CELTypeAdapter + return e, nil + } + opts := []cel.EnvOption{ + captureAdapt, + cel.Function("slice", + cel.MemberOverload("list_slice", + []*cel.Type{listType, cel.IntType, cel.IntType}, listType, + cel.FunctionBinding(func(args ...ref.Val) ref.Val { + list := args[0].(traits.Lister) + start := args[1].(types.Int) + end := args[2].(types.Int) + result, err := slice(adapt(), list, start, end) + if err != nil { + return types.WrapErr(err) + } + return result + }), + ), + ), + } + if lib.version >= 1 { + opts = append(opts, + cel.Function("flatten", + cel.MemberOverload("list_flatten", + []*cel.Type{listListType}, listType, + cel.UnaryBinding(func(arg ref.Val) ref.Val { + // double-check as type-guards disabled + list, ok := arg.(traits.Lister) + if !ok { + return types.ValOrErr(arg, "no such overload: %v.flatten()", arg.Type()) + } + flatList, err := flatten(list, 1) + if err != nil { + return types.WrapErr(err) + } + + return adapt().NativeToValue(flatList) + }), + ), + cel.MemberOverload("list_flatten_int", + []*cel.Type{listDyn, types.IntType}, listDyn, + cel.BinaryBinding(func(arg1, arg2 ref.Val) ref.Val { + // double-check as type-guards disabled + list, ok := arg1.(traits.Lister) + if !ok { + return types.ValOrErr(arg1, "no such overload: %v.flatten(%v)", arg1.Type(), arg2.Type()) + } + depth, ok := arg2.(types.Int) + if !ok { + return types.ValOrErr(arg1, "no such overload: %v.flatten(%v)", arg1.Type(), arg2.Type()) + } + flatList, err := flatten(list, int64(depth)) + if err != nil { + return types.WrapErr(err) + } + + return adapt().NativeToValue(flatList) + }), + ), + // To handle the case where a variable of just `list(T)` is provided at runtime + // with a graceful failure more, disable the type guards since the implementation + // can handle lists which are already flat. + decls.DisableTypeGuards(true), + ), + ) + } + if lib.version >= 2 { + sortDecl := cel.Function("sort", + append( + templatedOverloads(comparableTypes, func(t *cel.Type) cel.FunctionOpt { + return cel.MemberOverload( + fmt.Sprintf("list_%s_sort", t.TypeName()), + []*cel.Type{cel.ListType(t)}, cel.ListType(t), + ) + }), + cel.SingletonUnaryBinding( + func(arg ref.Val) ref.Val { + // validated by type-guards + list := arg.(traits.Lister) + sorted, err := sortList(adapt(), list) + if err != nil { + return types.WrapErr(err) + } + + return sorted + }, + // List traits + traits.ListerType, + ), + )..., + ) + opts = append(opts, sortDecl) + opts = append(opts, cel.Macros(cel.ReceiverMacro("sortBy", 2, sortByMacro))) + opts = append(opts, cel.Function("@sortByAssociatedKeys", + append( + templatedOverloads(comparableTypes, func(u *cel.Type) cel.FunctionOpt { + return cel.MemberOverload( + fmt.Sprintf("list_%s_sortByAssociatedKeys", u.TypeName()), + []*cel.Type{listType, cel.ListType(u)}, listType, + ) + }), + cel.SingletonBinaryBinding( + func(arg1, arg2 ref.Val) ref.Val { + // validated by type-guards + list := arg1.(traits.Lister) + keys := arg2.(traits.Lister) + sorted, err := sortListByAssociatedKeys(adapt(), list, keys) + if err != nil { + return types.WrapErr(err) + } + + return sorted + }, + // List traits + traits.ListerType, + ), + )..., + )) + + 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(adapt(), n.(types.Int), maxRange) + if err != nil { + return types.WrapErr(err) + } + return result + }), + ), + )) + opts = append(opts, cel.Function("reverse", + cel.MemberOverload("list_reverse", + []*cel.Type{listType}, listType, + cel.UnaryBinding(func(list ref.Val) ref.Val { + result, err := reverseList(adapt(), list.(traits.Lister)) + if err != nil { + return types.WrapErr(err) + } + return result + }), + ), + )) + opts = append(opts, cel.Function("distinct", + cel.MemberOverload("list_distinct", + []*cel.Type{listType}, listType, + cel.UnaryBinding(func(list ref.Val) ref.Val { + result, err := distinctList(adapt(), list.(traits.Lister)) + if err != nil { + return types.WrapErr(err) + } + return result + }), + ), + )) + } + if lib.version >= 3 { + legacy := lib.version == 3 + opts = append(opts, + cel.CostModel( + listSliceCostModel("list_slice"), + listsRangeCostModel("lists_range"), + listReverseCostModel("list_reverse"), + listDistinctCostModel("list_distinct", legacy), + listFlattenCostModel("list_flatten", legacy), + listFlattenDepthCostModel("list_flatten_int", legacy), + ), + cel.CostModel(listSortCostModels(legacy)...), + ) + } + if lib.version >= 5 { + opts = append(opts, + cel.Function("hasOnly", + cel.MemberOverload("list_hasOnly_list", + []*cel.Type{listType, listType}, cel.BoolType, + cel.BinaryBinding(listHasOnly), + ), + ), + cel.Function("hasAny", + cel.MemberOverload("list_hasAny_list", + []*cel.Type{listType, listType}, cel.BoolType, + cel.BinaryBinding(listHasAny), + ), + ), + cel.Function("hasAll", + cel.MemberOverload("list_hasAll_list", + []*cel.Type{listType, listType}, cel.BoolType, + cel.BinaryBinding(listHasAll), + ), + ), + cel.Function("hasExactly", + cel.MemberOverload("list_hasExactly_list", + []*cel.Type{listType, listType}, cel.BoolType, + cel.BinaryBinding(listHasExactly), + ), + ), + cel.CostModel( + listSetOpCostModel("list_hasOnly_list", 1), + listSetOpCostModel("list_hasAny_list", 1), + listSetOpCostModel("list_hasAll_list", 1), + listSetOpCostModel("list_hasExactly_list", 2), + ), + ) + } + + return opts +} + +// ProgramOptions implements the Library interface method. +func (lib *listsLib) ProgramOptions() []cel.ProgramOption { + return nil +} + +func genRange(adapt types.Adapter, 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([]int, 0, n) + for i := range int(n) { + newList = append(newList, i) + } + return adapt.NativeToValue(newList), nil +} + +func reverseList(adapt types.Adapter, list traits.Lister) (ref.Val, error) { + if reversed, ok := types.MaybeReverseList(adapt, list); ok { + return reversed, nil + } + listLength := list.Size().(types.Int) + newList := make([]ref.Val, 0, listLength) + for i := range listLength { + val := list.Get(listLength - i - 1) + newList = append(newList, val) + } + return adapt.NativeToValue(newList), nil +} + +func slice(adapt types.Adapter, list traits.Lister, start, end types.Int) (ref.Val, error) { + listLength := list.Size().(types.Int) + if start < 0 || end < 0 { + return nil, fmt.Errorf("cannot slice(%d, %d), negative indexes not supported", start, end) + } + if start > end { + return nil, fmt.Errorf("cannot slice(%d, %d), start index must be less than or equal to end index", start, end) + } + if listLength < end { + return nil, fmt.Errorf("cannot slice(%d, %d), list is length %d", start, end, listLength) + } + + if sliced, ok := types.MaybeSliceList(adapt, list, int(start), int(end)); ok { + return sliced, nil + } + + newList := make([]ref.Val, 0, end-start) + for i := types.Int(start); i < end; i++ { + val := list.Get(i) + newList = append(newList, val) + } + return adapt.NativeToValue(newList), nil +} + +func flatten(list traits.Lister, depth int64) ([]ref.Val, error) { + if depth < 0 { + return nil, fmt.Errorf("level must be non-negative") + } + + listLength := list.Size().(types.Int) + newList := make([]ref.Val, 0, listLength) + return flattenInto(newList, list, depth) +} + +func flattenInto(dst []ref.Val, list traits.Lister, depth int64) ([]ref.Val, error) { + listLength := list.Size().(types.Int) + for i := range listLength { + val := list.Get(i) + if depth == 0 { + dst = append(dst, val) + continue + } + if nestedList, isList := val.(traits.Lister); isList { + var err error + dst, err = flattenInto(dst, nestedList, depth-1) + if err != nil { + return nil, err + } + } else { + dst = append(dst, val) + } + } + return dst, nil +} + +func sortList(adapt types.Adapter, list traits.Lister) (ref.Val, error) { + return sortListByAssociatedKeys(adapt, list, list) +} + +// Internal function used for the implementation of sort() and sortBy(). +// +// Sorts a list of arbitrary elements, according to the order produced by sorting +// another list of comparable elements. If the element type of the keys is not +// comparable or the element types are not the same, the function will produce an error. +// +// .@sortByAssociatedKeys() -> +// U in {int, uint, double, bool, duration, timestamp, string, bytes} +// +// Example: +// +// ["foo", "bar", "baz"].@sortByAssociatedKeys([3, 1, 2]) // return ["bar", "baz", "foo"] +func sortListByAssociatedKeys(adapt types.Adapter, list, keys traits.Lister) (ref.Val, error) { + listLength := list.Size().(types.Int) + keysLength := keys.Size().(types.Int) + if listLength != keysLength { + return nil, fmt.Errorf( + "@sortByAssociatedKeys() expected a list of the same size as the associated keys list, but got %d and %d elements respectively", + listLength, + keysLength, + ) + } + if listLength == 0 { + return list, nil + } + elem := keys.Get(types.IntZero) + if _, ok := elem.(traits.Comparer); !ok { + return nil, fmt.Errorf("list elements must be comparable") + } + + sortedIndices := make([]ref.Val, 0, listLength) + for i := types.IntZero; i < listLength; i++ { + sortedIndices = append(sortedIndices, i) + } + + var err error + sort.Slice(sortedIndices, func(i, j int) bool { + iKey := keys.Get(sortedIndices[i]) + jKey := keys.Get(sortedIndices[j]) + if iKey.Type() != elem.Type() || jKey.Type() != elem.Type() { + err = fmt.Errorf("list elements must have the same type") + return false + } + return iKey.(traits.Comparer).Compare(jKey) == types.IntNegOne + }) + if err != nil { + return nil, err + } + + sorted := make([]ref.Val, 0, listLength) + for _, sortedIdx := range sortedIndices { + sorted = append(sorted, list.Get(sortedIdx)) + } + return adapt.NativeToValue(sorted), nil +} + +// sortByMacro transforms an expression like: +// +// mylistExpr.sortBy(e, -math.abs(e)) +// +// into something equivalent to: +// +// cel.bind( +// __sortBy_input__, +// myListExpr, +// __sortBy_input__.@sortByAssociatedKeys(__sortBy_input__.map(e, -math.abs(e)) +// ) +func sortByMacro(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) { + varIdent := meh.NewIdent("@__sortBy_input__") + varName := varIdent.AsIdent() + + targetKind := target.Kind() + if targetKind != ast.ListKind && + targetKind != ast.SelectKind && + targetKind != ast.IdentKind && + targetKind != ast.ComprehensionKind && + targetKind != ast.CallKind { + return nil, meh.NewError(target.ID(), "sortBy can only be applied to a list, identifier, comprehension, call or select expression") + } + + mapCompr, err := parser.MakeMap(meh, meh.Copy(varIdent), args) + if err != nil { + return nil, err + } + callExpr := meh.NewMemberCall("@sortByAssociatedKeys", + meh.Copy(varIdent), + mapCompr, + ) + + bindExpr := meh.NewComprehension( + meh.NewList(), + "#unused", + varName, + target, + meh.NewLiteral(types.False), + varIdent, + callExpr, + ) + + return bindExpr, nil +} + +func distinctList(adapt types.Adapter, list traits.Lister) (ref.Val, error) { + listLength := list.Size().(types.Int) + if listLength == 0 { + return list, nil + } + uniqueList := make([]ref.Val, 0, listLength) + for i := types.IntZero; i < listLength; i++ { + val := list.Get(i) + seen := false + for j := types.IntZero; j < types.Int(len(uniqueList)); j++ { + if i == j { + continue + } + other := uniqueList[j] + if val.Equal(other) == types.True { + seen = true + break + } + } + if !seen { + uniqueList = append(uniqueList, val) + } + } + + return adapt.NativeToValue(uniqueList), nil +} + +// listHasOnly returns true if every element of the target list is contained within the argument list. +func listHasOnly(list, other ref.Val) ref.Val { + return setsContains(other, list) +} + +// listHasAny returns true if the target list and the argument list share at least one element. +func listHasAny(list, other ref.Val) ref.Val { + return setsIntersects(list, other) +} + +// listHasAll returns true if every element of the argument list is contained within the target list. +func listHasAll(list, other ref.Val) ref.Val { + return setsContains(list, other) +} + +// listHasExactly returns true if the target list and the argument list are set equivalent. +func listHasExactly(list, other ref.Val) ref.Val { + return setsEquivalent(list, other) +} + +func templatedOverloads(types []*cel.Type, template func(t *cel.Type) cel.FunctionOpt) []cel.FunctionOpt { + overloads := make([]cel.FunctionOpt, len(types)) + for i, t := range types { + overloads[i] = template(t) + } + return overloads +} + +// estimateListSlice computes an O(n) slice operation with a cost factor of 1. +func estimateListSlice(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if target == nil || len(args) != 2 { + return nil + } + sz := cost.EstimateSize(estimator, *target) + start := cost.NodeAsUintValue(args[0], 0) + end := cost.NodeAsUintValue(args[1], sz.Max) + resSize := cost.FixedSizeEstimate(end - start) + resSize.Elem = sz.Elem + return cost.NewListCallEstimate(resSize.AsCost(), resSize) +} + +func flattenCallArgs(target *cost.AstNode, args []cost.AstNode) (depth uint64, anyDepth, ok bool) { + if target == nil || len(args) > 1 { + return 0, false, false + } + if len(args) == 0 { + return 1, false, true + } + if d, isLit := cost.UintValue(args[0]); isLit { + return d, false, true + } + return math.MaxUint, true, true +} + +// estimateListFlattenDepth estimates l.flatten(depth) under cost model version 1+. +func estimateListFlattenDepth(legacy bool) cost.FunctionEstimator { + return func(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + depth, anyDepth, ok := flattenCallArgs(target, args) + if !ok || len(args) != 1 { + return nil + } + factor := 1.0 + if legacy { + factor = float64(depth) + } + if est, ok := estimateFlattenByContainerSizing(estimator, *target, depth, factor, anyDepth); ok { + return est + } + resSize := estimateFlattenedList(estimator, *target, depth, anyDepth) + if !legacy { + return cost.NewListCallEstimate(resSize.AsCost(), resSize) + } + sz := cost.EstimateSize(estimator, *target) + work := sz.MultiplyByCostFactor(factor) + if anyDepth { + work.Min = 0 + } + return cost.NewListCallEstimate(work, resSize) + } +} + +// estimateListFlatten estimates l.flatten() and l.flatten(depth) under cost model version 0. +func estimateListFlatten(legacy bool) cost.FunctionEstimator { + return func(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + depth, anyDepth, ok := flattenCallArgs(target, args) + if !ok { + return nil + } + factor := 1.0 + if legacy { + factor = float64(depth) + } + if est, ok := estimateFlattenByContainerSizing(estimator, *target, depth, factor, anyDepth); ok { + return est + } + sz := cost.EstimateSize(estimator, *target) + if legacy { + return cost.NewListCallEstimate(sz.MultiplyByCostFactor(factor), sz) + } + var resSize cost.SizeEstimate + if litSize, ok := literalFlattenLength((*target).Expr(), depth); ok { + resSize = cost.FixedSizeEstimate(litSize.Max) + resSize.Elem = flattenedElemSize(sz, depth) + } else { + resSize = estimateFlattenSize(sz, (*target).Type(), depth) + } + return cost.NewListCallEstimate(resSize.AsCost(), resSize) + } +} + +// estimateFlattenByContainerSizing estimates flatten when container sizes are not lengths. +// +// flatten(depth) cannot be described by an OverloadModel, whose quantities cannot express a +// variable number of levels, so its estimator follows the sizing strategy itself. The estimators +// above derive the flattened length from the lengths at each level, which is right only when sizes +// are lengths. Otherwise: +// +// - When a container's size is its content (the aggregate strategy), flattening removes inner +// list headers and keeps every other value, so the result is no larger than the receiver. The +// receiver's length, and the flattened length the trackers charge, are each at most the +// receiver's size. costFactor scales the charged length: 1 for the flattened list, or the +// depth for the legacy tracker, which charges depth times the receiver's length. +// - When the relationship is not known, neither the size nor the length is bounded. +// +// It reports false when sizes are lengths, leaving the estimate to the caller. +func estimateFlattenByContainerSizing(estimator cost.Estimator, target cost.AstNode, depth uint64, costFactor float64, anyDepth bool) (*cost.CallEstimate, bool) { + switch cost.ContainerSizingOf(estimator) { + case cost.SizedByContent: + sz := cost.EstimateSize(estimator, target) + length := cost.RangedSizeEstimate(0, sz.Max) + resSize := cost.RangedSizeEstimate(min(1, sz.Max), sz.Max) + root := flattenLevel{t: target.Type(), sz: &sz} + resSize.Elem = flattenedElems(root, depth, anyDepth) + return cost.NewListCallEstimate(length.MultiplyByCostFactor(costFactor), resSize), true + case cost.SizedOpaque: + return cost.NewListCallEstimate(cost.UnknownCostEstimate(), cost.UnknownSizeEstimate()), true + default: + return nil, false + } +} + +func estimateFlattenSize(sz cost.SizeEstimate, tType *types.Type, depth uint64) cost.SizeEstimate { + res := cost.RangedSizeEstimate(sz.Min, sz.Max) + for i, cur := uint64(0), &sz; i < depth && tType.Kind() == types.ListKind && len(tType.Parameters()) > 0; i++ { + tType = tType.Parameters()[0] + cur = cur.Elem + if cur == nil { + res = res.Multiply(cost.UnknownSizeEstimate()) + break + } + res = res.Multiply(*cur) + } + res.Key = nil + res.Elem = flattenedElemSize(sz, depth) + return res +} + +func flattenedElemSize(sz cost.SizeEstimate, depth uint64) *cost.SizeEstimate { + cur := &sz + for i := uint64(0); i < depth && cur != nil; i++ { + cur = cur.Elem + } + if cur == nil { + return nil + } + return cur.Elem +} + +// The estimates below apply from cost model version 1, when sizes are lengths. They follow +// flattenInto, which descends into every value that is a list, whatever its static type, and keeps +// every other value as a single entry. estimateFlattenByContainerSizing covers the other sizing +// strategies, using flattenedElems for the result's element size. + +// maxFlattenLevels bounds how many levels of flattening the estimates follow: flatten(depth) is +// estimated for a depth of at most maxFlattenLevels, which reads the values up to one level +// further down. Flattening which may go deeper, by a larger depth or a non-literal one over a more +// deeply nested list, is estimated as unknown. +const maxFlattenLevels = 16 + +// flattenLevel describes the values at one level of a list being flattened: their static type and +// their size estimate, if known. Level 0 is the receiver. +type flattenLevel struct { + t *types.Type + sz *cost.SizeEstimate +} + +// next describes the values one level down: the elements of the lists at this level. +func (l flattenLevel) next() flattenLevel { + elemType := types.DynType + if l.t.Kind() == types.ListKind && len(l.t.Parameters()) > 0 { + elemType = l.t.Parameters()[0] + } + var sz *cost.SizeEstimate + if l.sz != nil { + sz = l.sz.Elem + } + return flattenLevel{t: elemType, sz: sz} +} + +// mayBeList reports whether a value at this level may be a list, which flattening expands. +func (l flattenLevel) mayBeList() bool { + return isDynamicFlattenType(l.t) || l.t.Kind() == types.ListKind +} + +// mayBeOther reports whether a value at this level may be something other than a list, which +// flattening keeps as a single entry. +func (l flattenLevel) mayBeOther() bool { + return isDynamicFlattenType(l.t) || l.t.Kind() != types.ListKind +} + +func isDynamicFlattenType(t *types.Type) bool { + switch t.Kind() { + case types.DynKind, types.AnyKind, types.TypeParamKind: + return true + } + return false +} + +// estimateFlattenedList bounds the list target.flatten(depth) produces when sizes are lengths. +// anyDepth reports a non-literal depth, which may take any value, so the bounds cover every depth. +func estimateFlattenedList(estimator cost.Estimator, target cost.AstNode, depth uint64, anyDepth bool) cost.SizeEstimate { + sz := cost.EstimateSize(estimator, target) + root := flattenLevel{t: target.Type(), sz: &sz} + expr := target.Expr() + var res cost.SizeEstimate + switch { + case anyDepth: + if litRes, ok := literalFlattenLengthAnyDepth(expr); ok { + res = litRes + } else { + res = flattenedLengthAnyDepth(root) + } + default: + if litRes, ok := literalFlattenLengthWithin(expr, depth); ok { + res = litRes + } else { + res = flattenedLength(root, depth, 0) + } + } + res.Key = nil + res.Elem = flattenedElems(root, depth, anyDepth) + return res +} + +// flattenedLength bounds the number of entries flattening the list at level l yields, descending +// into depth more levels. A list contributes its flattened length, and any other value 1. +func flattenedLength(l flattenLevel, depth uint64, levels int) cost.SizeEstimate { + if l.sz == nil || levels > maxFlattenLevels { + return cost.UnknownSizeEstimate() + } + if depth == 0 { + return cost.RangedSizeEstimate(l.sz.Min, l.sz.Max) + } + elems := l.next() + var per *cost.SizeEstimate + if elems.mayBeOther() { + one := cost.FixedSizeEstimate(1) + per = &one + } + if elems.mayBeList() { + inner := flattenedLength(elems, depth-1, levels+1) + per = unionBounds(per, inner) + } + return cost.RangedSizeEstimate( + cost.SafeMultiply(l.sz.Min, per.Min), + cost.SafeMultiply(l.sz.Max, per.Max)) +} + +// flattenedLengthAnyDepth bounds the flattened length over every depth. Flattening below the last +// level which may hold a list changes nothing, so the depths up to that level suffice. +func flattenedLengthAnyDepth(root flattenLevel) cost.SizeEstimate { + res := flattenedLength(root, 0, 0) + level := root.next() + for d := uint64(1); level.mayBeList(); d++ { + if d > maxFlattenLevels { + return cost.UnknownSizeEstimate() + } + res = *unionBounds(&res, flattenedLength(root, d, 0)) + if res.IsUnknown() { + return res + } + level = level.next() + } + return res +} + +// flattenedElems bounds the elements of the flattened list: the values at levels 1 to depth which +// are not lists, and every value at level depth+1. When anyDepth is set, a value at any level may +// be kept. A level whose values may be kept, but whose size is not known, makes the elements +// unknown. +func flattenedElems(root flattenLevel, depth uint64, anyDepth bool) *cost.SizeEstimate { + var res *cost.SizeEstimate + level := root + for k := uint64(1); ; k++ { + // flatten(maxFlattenLevels) keeps the values one level below its deepest expansion. + if k > maxFlattenLevels+1 { + return unknownSize() + } + level = level.next() + last := !anyDepth && k == depth+1 + if anyDepth || last || level.mayBeOther() { + if level.sz == nil { + return unknownSize() + } + res = unionBounds(res, *level.sz) + } + if last || !level.mayBeList() { + return res + } + } +} + +// literalFlattenLength bounds the flattened length of a list literal when all expanded elements up +// to depth are literals or list literals. An optional element may be absent, so it counts toward +// the upper bound only. +func literalFlattenLength(expr ast.Expr, depth uint64) (cost.SizeEstimate, bool) { + if expr == nil { + return cost.SizeEstimate{}, false + } + if expr.Kind() != ast.ListKind { + if expr.Kind() == ast.LiteralKind { + return cost.FixedSizeEstimate(1), true + } + return cost.SizeEstimate{}, false + } + list := expr.AsList() + optional := make(map[int32]bool, len(list.OptionalIndices())) + for _, i := range list.OptionalIndices() { + optional[i] = true + } + var lo, hi uint64 + for i, el := range list.Elements() { + elSize := cost.FixedSizeEstimate(1) + if depth > 0 { + var ok bool + if elSize, ok = literalFlattenLength(el, depth-1); !ok { + return cost.SizeEstimate{}, false + } + } + if optional[int32(i)] { + elSize.Min = 0 + } + lo, hi = cost.SafeAdd(lo, elSize.Min), cost.SafeAdd(hi, elSize.Max) + } + return cost.RangedSizeEstimate(lo, hi), true +} + +// listLiteralLevels reports how many levels of list literals expr nests: 0 when expr is not a list +// literal. Flattening a literal of n levels by more than n-1 changes nothing. +func listLiteralLevels(expr ast.Expr) uint64 { + if expr == nil || expr.Kind() != ast.ListKind { + return 0 + } + var deepest uint64 + for _, el := range expr.AsList().Elements() { + deepest = max(deepest, listLiteralLevels(el)) + } + return deepest + 1 +} + +// literalFlattenLengthWithin is literalFlattenLength for flattening which expands at most +// maxFlattenLevels levels of the literal. +func literalFlattenLengthWithin(expr ast.Expr, depth uint64) (cost.SizeEstimate, bool) { + if levels := listLiteralLevels(expr); levels == 0 || min(depth, levels-1) > maxFlattenLevels { + return cost.SizeEstimate{}, false + } + return literalFlattenLength(expr, depth) +} + +// literalFlattenLengthAnyDepth bounds the flattened length of a fully literal list over every +// depth: the depths which expand a level of the literal, of which there may be at most +// maxFlattenLevels. +func literalFlattenLengthAnyDepth(expr ast.Expr) (cost.SizeEstimate, bool) { + levels := listLiteralLevels(expr) + if levels == 0 || levels-1 > maxFlattenLevels { + return cost.SizeEstimate{}, false + } + var res *cost.SizeEstimate + for d := uint64(0); d < levels; d++ { + sz, ok := literalFlattenLength(expr, d) + if !ok { + return cost.SizeEstimate{}, false + } + res = unionBounds(res, sz) + } + return *res, true +} + +// unionBounds returns an estimate covering both a, which may be nil, and b, along with the +// union of their metadata. +func unionBounds(a *cost.SizeEstimate, b cost.SizeEstimate) *cost.SizeEstimate { + if a == nil { + return &b + } + u := a.Union(b) + return &u +} + +func unknownSize() *cost.SizeEstimate { + u := cost.UnknownSizeEstimate() + return &u +} + +// estimateListDistinct estimates l.distinct() under ModelVersion0. +func estimateListDistinct(legacy bool) cost.FunctionEstimator { + return func(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if target == nil || len(args) != 0 { + return nil + } + elemType := types.DynType + if t := (*target).Type(); t.Kind() == types.ListKind && len(t.Parameters()) > 0 { + elemType = t.Parameters()[0] + } + return estimateSelfCompareCost(estimator, *target, *target, elemType, legacy, !legacy) + } +} + +// estimateListSort estimates l.sort() under ModelVersion0. +func estimateListSort(t *types.Type, legacy bool) cost.FunctionEstimator { + return func(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if target == nil || len(args) != 0 { + return nil + } + return estimateSelfCompareCost(estimator, *target, *target, t, legacy, false) + } +} + +// estimateListSortBy estimates l.sortBy() under ModelVersion0. +func estimateListSortBy(u *types.Type, legacy bool) cost.FunctionEstimator { + return func(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if target == nil || len(args) != 1 { + return nil + } + return estimateSelfCompareCost(estimator, args[0], *target, u, legacy, false) + } +} + +func estimateSelfCompareCost(estimator cost.Estimator, keysNode, targetNode cost.AstNode, elemType *types.Type, legacy, clampMin bool) *cost.CallEstimate { + sz := cost.EstimateSize(estimator, keysNode) + targetSize := cost.EstimateSize(estimator, targetNode) + costSize := sz.Multiply(sz) + if legacy { + costSize.Elem = targetSize.Elem + return cost.NewListCallEstimate(costSize.MultiplyByCostFactor(selfCompareFactor(elemType)), costSize) + } + elemCost := estimateElementEqualityCost(elemType, estimateItemSize(estimator, targetNode)) + c := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + resSize := cost.RangedSizeEstimate(sz.Min, sz.Max) + if clampMin && sz.Min > 0 { + resSize.Min = 1 + } + resSize.Elem = targetSize.Elem + return cost.NewListCallEstimate(c, resSize) +} + +func estimateItemSize(estimator cost.Estimator, node cost.AstNode) cost.SizeEstimate { + if len(node.Path()) == 0 { + return cost.UnknownSizeEstimate() + } + elemType := types.DynType + if t := node.Type(); t.Kind() == types.ListKind && len(t.Parameters()) > 0 { + elemType = t.Parameters()[0] + } + if l := estimator.EstimateSize(cost.NewAstNode(nil, append(slices.Clone(node.Path()), "@items"), elemType, nil)); l != nil { + return *l + } + return cost.UnknownSizeEstimate() +} + +func estimateElementEqualityCost(elemType *types.Type, itemSize cost.SizeEstimate) cost.CostEstimate { + switch elemType.Kind() { + case types.StringKind, types.BytesKind: + return itemSize.MultiplyByCostFactor(cost.StringTraversalCostFactor) + case types.ListKind, types.MapKind, types.StructKind: + return cost.UnknownCostEstimate() + default: + return cost.FixedCostEstimate(1) + } +} + +// trackListSelfCompare computes the ModelVersion0 O(n^2) runtime charge over args[argIdx]. +func trackListSelfCompare(argIdx int) cost.FunctionTracker { + return func(args []ref.Val, _ ref.Val) *uint64 { + l := args[argIdx].(traits.Lister) + sz := cost.ActualSize(l) + costFactor := 2.0 + if sz > 0 { + if elem := l.Get(types.IntZero); elem.Type() == types.StringType || elem.Type() == types.BytesType { + costFactor += cost.StringTraversalCostFactor + } + } + // The scaled size is truncated, matching the historical cost for in-range values, but + // saturates rather than relying on a platform-defined out-of-range float conversion. + total := cost.SafeAdd(cost.SafeMultiplyByFactorTrunc(cost.SafeMultiply(sz, sz), costFactor), cost.CallCost, cost.ListCreateBaseCost) + return &total + } +} + +// listSliceCostModel models l.slice(start, end): it copies end - start elements of the receiver, +// and its size is also at most the receiver's. +func listSliceCostModel(id string) cost.OverloadModel { + sliceList := cost.List(cost.Sub(cost.IntArg(1, 0), cost.IntArg(0, 0)), cost.TargetElem()) + return cost.MemberOverload(id, + cost.EvalCost(cost.AllocList(cost.Length(cost.Result()))), + cost.ResultSize(cost.Ranged(sliceList, cost.Min(sliceList, cost.Target()))), + cost.BeforeVersion(1, cost.ManualEstimator(estimateListSlice)), + ) +} + +// listsRangeCostModel models lists.range(n): it builds n ints, each of size 1 under both built-in +// strategies. +func listsRangeCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.AllocList(cost.Length(cost.Result()))), + cost.ResultSize(cost.List(cost.IntArg(0, math.MaxUint), cost.Const(1))), + ) +} + +func listReverseCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.AllocList(cost.Length(cost.Result()))), + cost.ResultSize(cost.Target()), + ) +} + +// listDistinctCostModel models l.distinct(): it compares every pair of elements. Its tracker +// raises the factor for string and bytes elements, which a model of the single list(dyn) overload +// cannot see, so the model charges the larger factor. The result holds some of the receiver's +// elements, and at least one when the receiver has any. +func listDistinctCostModel(id string, legacy bool) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(selfCompareCost(cost.Length(cost.Target()), 2+cost.StringTraversalCostFactor)), + cost.ResultSize(cost.Ranged(cost.Min(cost.Target(), cost.Const(1)), cost.Target())), + cost.BeforeVersion(1, cost.ManualCost(estimateListDistinct(legacy), trackListSelfCompare(0))), + ) +} + +// flattenedSize bounds the size of l.flatten(), sound under both built-in strategies. Its upper +// bound is the smaller of two bounds: +// +// - Under the default strategy each element contributes at most max(its size, 1) to the +// flattened length: a list its length, any other value 1. The first bound is the receiver's +// length times that. +// - Under the aggregate strategy the flattened list holds the receiver's contents less the inner +// list headers, so its size is at most the receiver's size plus its content size, the second +// bound. The second bound also bounds the default strategy's length, as the receiver's length +// plus the summed element sizes. +// +// Its lower bound is 0: no bound in terms of the receiver's size and element size holds under +// both strategies, since the inner lists may be empty. +func flattenedSize() cost.QuantityExpr { + return cost.AtMost(cost.Min( + cost.List( + cost.Mul(cost.Target(), cost.AtLeastOneQuantity(cost.TargetElem())), + cost.ElemOf(cost.TargetElem()), + ), + cost.Sum(cost.Target(), cost.ElemTotal(cost.Target())), + )) +} + +func listFlattenCostModel(id string, legacy bool) cost.OverloadModel { + work := cost.Length(cost.Result()) + if legacy { + work = cost.Length(cost.Target()) + } + return cost.MemberOverload(id, + cost.EvalCost(cost.AllocList(work)), + cost.ResultSize(flattenedSize()), + cost.BeforeVersion(1, cost.ManualEstimator(estimateListFlatten(legacy))), + ) +} + +func listFlattenDepthCostModel(id string, legacy bool) cost.OverloadModel { + work := cost.Length(cost.Result()) + if legacy { + work = cost.Mul(cost.Length(cost.Target()), cost.IntArg(0, 1)) + } + return cost.MemberOverload(id, + cost.EvalCost(cost.AllocList(work)), + cost.ManualEstimator(estimateListFlattenDepth(legacy)), + cost.BeforeVersion(1, cost.ManualEstimator(estimateListFlatten(legacy))), + ) +} + +// selfCompareCost is the cost of comparing each element of a list of the given length against +// every other, the charge of the sort and distinct trackers. +func selfCompareCost(length cost.QuantityExpr, factor float64) cost.QuantityExpr { + return cost.AllocList(cost.Scale(cost.Mul(length, length), factor)) +} + +// selfCompareFactor is the comparison factor of an element type: string and bytes comparisons +// also traverse the values. +func selfCompareFactor(t *types.Type) float64 { + switch t.Kind() { + case types.StringKind, types.BytesKind: + return 2 + cost.StringTraversalCostFactor + default: + return 2 + } +} + +func listSortCostModels(legacy bool) []cost.OverloadModel { + models := make([]cost.OverloadModel, 0, 2*len(comparableTypes)) + for _, t := range comparableTypes { + f := selfCompareFactor(t) + models = append(models, + cost.MemberOverload(fmt.Sprintf("list_%s_sort", t.TypeName()), + cost.EvalCost(selfCompareCost(cost.Length(cost.Target()), f)), + cost.ResultSize(cost.Target()), + cost.BeforeVersion(1, cost.ManualCost(estimateListSort(t, legacy), trackListSelfCompare(0))), + ), + // sortBy compares the keys, and reorders the receiver. + cost.MemberOverload(fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), + cost.EvalCost(selfCompareCost(cost.Length(cost.Arg(0)), f)), + cost.ResultSize(cost.Target()), + cost.BeforeVersion(1, cost.ManualCost(estimateListSortBy(t, legacy), trackListSelfCompare(1))), + ), + ) + } + return models +} + +func listSetOpCostModel(id string, costFactor float64) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), + cost.Scale(cost.Mul(cost.Length(cost.Target()), cost.Length(cost.Arg(0))), costFactor))), + ) +} diff --git a/vendor/github.com/google/cel-go/ext/math.go b/vendor/cel.dev/cel-go/ext/math.go similarity index 94% rename from vendor/github.com/google/cel-go/ext/math.go rename to vendor/cel.dev/cel-go/ext/math.go index 6df8e377369..ea435d32dcd 100644 --- a/vendor/github.com/google/cel-go/ext/math.go +++ b/vendor/cel.dev/cel-go/ext/math.go @@ -19,11 +19,12 @@ import ( "math" "strings" - "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" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // Math returns a cel.EnvOption to configure namespaced math helper macros and @@ -339,9 +340,9 @@ import ( // // Examples: // -// math.sqrt(81) // returns 9.0 -// math.sqrt(985.25) // returns 31.388692231439016 -// math.sqrt(-15) // returns NaN +// math.sqrt(81) // returns 9.0 +// math.sqrt(985.25) // returns 31.388692231439016 +// math.sqrt(-15) // returns NaN func Math(options ...MathOption) cel.EnvOption { m := &mathLib{version: math.MaxUint32} for _, o := range options { @@ -580,12 +581,24 @@ func (lib *mathLib) CompileOptions() []cel.EnvOption { ), ) } + if lib.version >= 3 { + opts = append(opts, + cel.CostModel( + mathListCostModel("math_@min_list_double"), + mathListCostModel("math_@min_list_int"), + mathListCostModel("math_@min_list_uint"), + mathListCostModel("math_@max_list_double"), + mathListCostModel("math_@max_list_int"), + mathListCostModel("math_@max_list_uint"), + ), + ) + } return opts } // ProgramOptions implements the Library interface method. -func (*mathLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{} +func (lib *mathLib) ProgramOptions() []cel.ProgramOption { + return nil } func mathLeast(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) { @@ -723,21 +736,19 @@ func sign(val ref.Val) ref.Val { } } - func sqrt(val ref.Val) ref.Val { switch v := val.(type) { case types.Double: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) case types.Int: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) case types.Uint: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) default: - return types.NewErr("no such overload: sqrt") + return types.NewErr("no such overload: sqrt") } } - func bitAndPairInt(first, second ref.Val) ref.Val { l := first.(types.Int) r := second.(types.Int) @@ -946,3 +957,27 @@ func maybeSuffixError(val ref.Val, suffix string) ref.Val { } return val } + +func estimateMathListCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) != 1 { + return nil + } + sz := cost.EstimateSize(estimator, args[0]) + c := sz.MultiplyByCostFactor(1.0).Add(cost.CallCostEstimate) + resultSize := cost.FixedSizeEstimate(1) + return cost.NewCallEstimate(c, &resultSize) +} + +func trackMathListCost(args []ref.Val, _ ref.Val) *uint64 { + sz := cost.ActualSize(args[0]) + total := cost.SafeAdd(sz, cost.CallCost) + return &total +} + +func mathListCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.Length(cost.Arg(0)))), + cost.ResultSize(cost.Const(1)), + cost.BeforeVersion(1, cost.ManualCost(estimateMathListCost, trackMathListCost)), + ) +} diff --git a/vendor/cel.dev/cel-go/ext/native.go b/vendor/cel.dev/cel-go/ext/native.go new file mode 100644 index 00000000000..386b988e1d5 --- /dev/null +++ b/vendor/cel.dev/cel-go/ext/native.go @@ -0,0 +1,134 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ext + +import ( + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" +) + +// NativeTypesOption is a functional interface for configuring handling of native types. +type NativeTypesOption = types.NativeTypeOption + +// NativeTypesFieldNameHandler is a handler for mapping a reflect.StructField to a CEL field name. +// This can be used to override the default Go struct field to CEL field name mapping. +type NativeTypesFieldNameHandler = types.NativeTypesFieldNameHandler + +var ( + // ParseStructTags configures if native types field names should be overridable by CEL struct tags. + // This is equivalent to ParseStructTag("cel") + ParseStructTags = types.ParseStructTags + + // ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field. + ParseStructTag = types.ParseStructTag + + // ParseStructField configures how to parse Go struct fields. It can be used to customize struct field parsing. + ParseStructField = types.ParseStructField +) + +// NativeTypesVersion sets the native types version support for native extensions functions. +// +// Deprecated: NativeTypesVersion is a no-op and will be removed in a future release. +func NativeTypesVersion(version uint32) NativeTypesOption { + return func(*types.NativeTypeOptions) error { + return nil + } +} + +// NativeTypes creates a type provider which uses reflect.Type and reflect.Value instances +// to produce type definitions that can be used within CEL. +// +// All struct types in Go are exposed to CEL via their simple package name and struct type name: +// +// ```go +// package identity +// +// type Account struct { +// ID int +// } +// +// ``` +// +// The type `identity.Account` would be exported to CEL using the same qualified name, e.g. +// `identity.Account{ID: 1234}` would create a new `Account` instance with the `ID` field +// populated. +// +// Only exported fields are exposed via NativeTypes, and the type-mapping between Go and CEL +// is as follows: +// +// | Go type | CEL type | +// |-------------------------------------|-----------| +// | bool | bool | +// | []byte | bytes | +// | float32, float64 | double | +// | int, int8, int16, int32, int64 | int | +// | string | string | +// | uint, uint8, uint16, uint32, uint64 | uint | +// | time.Duration | duration | +// | time.Time | timestamp | +// | array, slice | list | +// | map | map | +// +// Please note, if you intend to configure support for proto messages in addition to native +// types, you will need to provide the protobuf types before the golang native types. The +// same advice holds if you are using custom type adapters and type providers. The native type +// provider composes over whichever type adapter and provider is configured in the cel.Env at +// the time that it is invoked. +// +// There is also the possibility to rename the fields of native structs by setting the `cel` tag +// for fields you want to override. In order to enable this feature, pass in the `ParseStructTags(true)` +// option. Here is an example to see it in action: +// +// ```go +// package identity +// +// type Credentials struct { +// APIKey string +// } +// +// type Account struct { +// Credentials `cel:"-" json:"-"` +// ID int +// OwnerName string `cel:"owner"` +// Password string `cel:"-" json:"-"` +// } +// +// ``` +// +// The `OwnerName` field is now accessible in CEL via `owner`, e.g. `identity.Account{owner: 'bob'}`. +// In case there are duplicated field names in the struct, an error will be returned. +// +// A `cel:"-"` tag hides a field from CEL expressions entirely. Hiding an embedded struct also +// hides the fields promoted from it, so neither `account.Credentials` nor `account.APIKey` can +// be referenced above. +// +// Note that the `cel` tag only controls what CEL expressions can see. Encoding a native object, +// whether through the `json.encode` / `yaml.encode` extension functions or through conversion to +// a protobuf `Struct` or `Value`, follows the `json` tag instead. To keep a field out of both +// CEL and encoded output, tag it with both `cel:"-"` and `json:"-"` as shown above. Alternatively, +// configure `ParseStructTag("json")` so that a single tag governs both. +func NativeTypes(args ...any) cel.EnvOption { + return func(env *cel.Env) (*cel.Env, error) { + p, a, err := types.ComposeTypes(env.CELTypeProvider(), env.CELTypeAdapter(), args...) + if err != nil { + return nil, err + } + env, err = cel.CustomTypeAdapter(a)(env) + if err != nil { + return nil, err + } + return cel.CustomTypeProvider(p)(env) + } +} diff --git a/vendor/github.com/google/cel-go/ext/network.go b/vendor/cel.dev/cel-go/ext/network.go similarity index 61% rename from vendor/github.com/google/cel-go/ext/network.go rename to vendor/cel.dev/cel-go/ext/network.go index affe59e2d35..af9946948b7 100644 --- a/vendor/github.com/google/cel-go/ext/network.go +++ b/vendor/cel.dev/cel-go/ext/network.go @@ -16,13 +16,15 @@ package ext import ( "fmt" + "math" "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" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) const ( @@ -182,7 +184,11 @@ const ( var ( // Definitions for the Opaque Types - IPType = types.NewOpaqueType("net.IP") + + // IPType represents a network IP address. + IPType = types.NewOpaqueType("net.IP") + + // CIDRType represents a CIDR-format network range. CIDRType = types.NewOpaqueType("net.CIDR") ) @@ -196,13 +202,11 @@ func (*networkLib) LibraryName() string { 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, @@ -288,6 +292,29 @@ func (*networkLib) CompileOptions() []cel.EnvOption { networkFormatValidator{funcName: ipFunc, argNum: 0, check: checkIP}, networkFormatValidator{funcName: cidrFunc, argNum: 0, check: checkCIDR}, ), + cel.CostModel( + networkParseCostModel("string_to_ip"), + networkParseCostModel("string_to_cidr"), + networkParseBoolCostModel("is_ip"), + networkParseBoolCostModel("is_cidr"), + networkIsCanonicalCostModel("ip_is_canonical"), + networkToStringCostModel("ip_to_string"), + networkToStringCostModel("cidr_to_string"), + networkCIDROpaqueCostModel("cidr_ip"), + networkCIDROpaqueCostModel("cidr_masked"), + networkContainsIPIPCostModel("cidr_contains_ip_ip"), + networkContainsIPStringCostModel("cidr_contains_ip_string"), + networkContainsCIDRCIDRCostModel("cidr_contains_cidr"), + networkContainsCIDRStringCostModel("cidr_contains_cidr_string"), + networkNominalCostModel("ip_family"), + networkNominalCostModel("ip_is_global_unicast"), + networkNominalCostModel("ip_is_link_local_multicast"), + networkNominalCostModel("ip_is_link_local_unicast"), + networkNominalCostModel("ip_is_loopback"), + networkNominalCostModel("ip_is_unspecified"), + networkNominalCostModel("cidr_is_mask"), + networkNominalCostModel("cidr_prefix_length"), + ), } } @@ -478,8 +505,7 @@ func parseIPAddr(raw string) (netip.Addr, error) { return addr, nil } -// --- Opaque Type Wrappers --- - +// IP represents an IP address type. type IP struct { netip.Addr } @@ -527,6 +553,13 @@ func (i IP) Value() any { return i.Addr } +// Size returns the size of the IP address in bytes. +// /Used in the size estimation of the runtime cost. +func (i IP) Size() ref.Val { + return types.Int(int64(math.Ceil(float64(i.Addr.BitLen()) / 8))) +} + +// CIDR represents the CIDR network mask format. type CIDR struct { netip.Prefix } @@ -574,6 +607,12 @@ func (c CIDR) Value() any { return c.Prefix } +// Size returns the size of the CIDR prefix address in bytes. +// Used in the size estimation of the runtime cost. +func (c CIDR) Size() ref.Val { + return types.Int(int64(math.Ceil(float64(c.Prefix.Bits()) / 8))) +} + // --- Static Validators --- type argChecker func(e *cel.Env, call, arg ast.Expr) error @@ -617,3 +656,220 @@ func checkCIDR(e *cel.Env, call, arg ast.Expr) error { _, err := parseCIDR(pattern) return err } + +// Cost estimation functions for network extensions. + +func estimateNetworkParseCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) < 1 { + return nil + } + sz := cost.EstimateSize(estimator, args[0]) + resultSize := cost.RangedSizeEstimate(4, 16) + return cost.NewCallEstimate(sz.MultiplyByCostFactor(cost.StringCostFactor), &resultSize) +} + +func estimateNetworkParseBoolCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) < 1 { + return nil + } + sz := cost.EstimateSize(estimator, args[0]) + return cost.NewCallEstimate(sz.MultiplyByCostFactor(cost.StringCostFactor), nil) +} + +func estimateIPIsCanonicalCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) < 1 { + return nil + } + sz := cost.EstimateSize(estimator, args[0]) + return cost.NewCallEstimate(sz.MultiplyByCostFactor(2*cost.StringCostFactor), nil) +} + +func estimateNetworkNominalCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + return cost.NewCallEstimate(cost.CallCostEstimate, nil) +} + +func estimateNetworkNominalOpaqueCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + resultSize := cost.RangedSizeEstimate(4, 16) + return cost.NewCallEstimate(cost.CallCostEstimate, &resultSize) +} + +func estimateNetworkNominalStringCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + resultSize := cost.RangedSizeEstimate(3, 45) + return cost.NewCallEstimate(cost.CallCostEstimate, &resultSize) +} + +func estimateNetworkContainsIPIPCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + sz := cost.RangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(cost.StringCostFactor) + return cost.NewCallEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsIPStringCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) < 1 { + return nil + } + sz := cost.RangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(cost.StringCostFactor) + argSz := cost.EstimateSize(estimator, args[0]) + ipCompCost = ipCompCost.Add(argSz.MultiplyByCostFactor(cost.StringCostFactor)) + return cost.NewCallEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsCIDRCIDRCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + sz := cost.RangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(cost.StringCostFactor) + ipCompCost = ipCompCost.Add(sz.MultiplyByCostFactor(cost.StringCostFactor)) + // K8s adds one for the extra IP traversal + ipCompCost = ipCompCost.Add(cost.CallCostEstimate) + return cost.NewCallEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsCIDRStringCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) < 1 { + return nil + } + sz := cost.RangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(cost.StringCostFactor) + ipCompCost = ipCompCost.Add(sz.MultiplyByCostFactor(cost.StringCostFactor)) + argSz := cost.EstimateSize(estimator, args[0]) + ipCompCost = ipCompCost.Add(argSz.MultiplyByCostFactor(cost.StringCostFactor)) + // K8s adds one for the extra IP traversal + ipCompCost = ipCompCost.Add(cost.CallCostEstimate) + return cost.NewCallEstimate(ipCompCost, nil) +} + +// Runtime cost tracking functions for network extensions. + +func trackNetworkParseCost(args []ref.Val, result ref.Val) *uint64 { + total := cost.SafeMultiplyByFactor(cost.ActualSize(args[0]), cost.StringCostFactor) + return &total +} + +func trackIPIsCanonicalCost(args []ref.Val, result ref.Val) *uint64 { + total := cost.SafeMultiplyByFactor(cost.ActualSize(args[0]), 2*cost.StringCostFactor) + return &total +} + +func trackNetworkNominalCost(args []ref.Val, result ref.Val) *uint64 { + return &cost.CallCost +} + +func trackNetworkContainsIPIPCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := cost.ActualSize(args[0]) + total := cost.SafeMultiplyByFactor(cost.SafeAdd(cidrSize, cidrSize), cost.StringCostFactor) + return &total +} + +func trackNetworkContainsIPStringCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := cost.ActualSize(args[0]) + otherSize := cost.ActualSize(args[1]) + total := cost.SafeMultiplyByFactor(cost.SafeAdd(cidrSize, cidrSize), cost.StringCostFactor) + total = cost.SafeAdd(total, cost.SafeMultiplyByFactor(otherSize, cost.StringCostFactor)) + return &total +} + +func trackNetworkContainsCIDRCIDRCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := cost.ActualSize(args[0]) + total := cost.SafeMultiplyByFactor(cost.SafeAdd(cidrSize, cidrSize), cost.StringCostFactor) + total = cost.SafeAdd(total, cost.SafeMultiplyByFactor(cidrSize, cost.StringCostFactor), 1) + return &total +} + +func trackNetworkContainsCIDRStringCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := cost.ActualSize(args[0]) + otherSize := cost.ActualSize(args[1]) + total := cost.SafeMultiplyByFactor(cost.SafeAdd(cidrSize, cidrSize), cost.StringCostFactor) + total = cost.SafeAdd(total, cost.SafeMultiplyByFactor(cidrSize, cost.StringCostFactor), 1) + total = cost.SafeAdd(total, cost.SafeMultiplyByFactor(otherSize, cost.StringCostFactor)) + return &total +} + +// networkParseCostModel models ip(string) and cidr(string): an IP's length is its address bytes +// (4 or 16) and a CIDR's is the bytes of its prefix (0 to 16); both strategies size them by that +// length, so parsed values are sized at most 16 with no lower bound. +func networkParseCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.StringScan(cost.Length(cost.Arg(0)))), + cost.ResultSize(cost.AtMost(cost.Const(16))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkParseCost, trackNetworkParseCost)), + ) +} + +func networkParseBoolCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.StringScan(cost.Length(cost.Arg(0)))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkParseBoolCost, trackNetworkParseCost)), + ) +} + +func networkIsCanonicalCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.Scale(cost.Length(cost.Arg(0)), 2*cost.StringCostFactor)), + cost.BeforeVersion(1, cost.ManualCost(estimateIPIsCanonicalCost, trackIPIsCanonicalCost)), + ) +} + +// networkToStringCostModel models string(IP) and string(CIDR): formatted IPv6/CIDR strings are at +// most 45 characters. +func networkToStringCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.Const(cost.CallCost)), + cost.ResultSize(cost.AtMost(cost.Const(45))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkNominalStringCost, trackNetworkNominalCost)), + ) +} + +func networkCIDROpaqueCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Const(cost.CallCost)), + cost.ResultSize(cost.AtMost(cost.Const(16))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkNominalOpaqueCost, trackNetworkNominalCost)), + ) +} + +func networkNominalCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Const(cost.CallCost)), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkNominalCost, trackNetworkNominalCost)), + ) +} + +// networkCIDRLength returns the bounded length of a CIDR receiver (at most 16 bytes), which bounds +// the estimate for a receiver of unknown size without changing the runtime charge. +func networkCIDRLength() cost.QuantityExpr { + return cost.Min(cost.Length(cost.Target()), cost.Const(16)) +} + +// networkCIDRCompareCost returns the address comparison cost for a CIDR receiver, which reads the +// CIDR twice and therefore scans at twice StringCostFactor. +func networkCIDRCompareCost() cost.QuantityExpr { + return cost.Scale(networkCIDRLength(), 2*cost.StringCostFactor) +} + +func networkContainsIPIPCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(networkCIDRCompareCost()), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkContainsIPIPCost, trackNetworkContainsIPIPCost)), + ) +} + +func networkContainsIPStringCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(networkCIDRCompareCost(), cost.StringScan(cost.Length(cost.Arg(0))))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkContainsIPStringCost, trackNetworkContainsIPStringCost)), + ) +} + +func networkContainsCIDRCIDRCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(networkCIDRCompareCost(), cost.StringScan(networkCIDRLength()), cost.Const(1))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkContainsCIDRCIDRCost, trackNetworkContainsCIDRCIDRCost)), + ) +} + +func networkContainsCIDRStringCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(networkCIDRCompareCost(), cost.StringScan(networkCIDRLength()), cost.Const(1), cost.StringScan(cost.Length(cost.Arg(0))))), + cost.BeforeVersion(1, cost.ManualCost(estimateNetworkContainsCIDRStringCost, trackNetworkContainsCIDRStringCost)), + ) +} diff --git a/vendor/github.com/google/cel-go/ext/protos.go b/vendor/cel.dev/cel-go/ext/protos.go similarity index 98% rename from vendor/github.com/google/cel-go/ext/protos.go rename to vendor/cel.dev/cel-go/ext/protos.go index b09db25b09e..0dc2f6a0daa 100644 --- a/vendor/github.com/google/cel-go/ext/protos.go +++ b/vendor/cel.dev/cel-go/ext/protos.go @@ -17,8 +17,8 @@ package ext import ( "math" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/ast" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" ) // Protos returns a cel.EnvOption to configure extended macros and functions for diff --git a/vendor/github.com/google/cel-go/ext/regex.go b/vendor/cel.dev/cel-go/ext/regex.go similarity index 58% rename from vendor/github.com/google/cel-go/ext/regex.go rename to vendor/cel.dev/cel-go/ext/regex.go index bd222f170ee..17f4d72885c 100644 --- a/vendor/github.com/google/cel-go/ext/regex.go +++ b/vendor/cel.dev/cel-go/ext/regex.go @@ -22,12 +22,10 @@ import ( "strconv" "strings" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) const ( @@ -154,11 +152,11 @@ func (r *regexLib) CompileOptions() []cel.EnvOption { cel.Overload("regex_replace_string_string_string_int", []*cel.Type{cel.StringType, cel.StringType, cel.StringType, cel.IntType}, cel.StringType, cel.FunctionBinding((regReplaceN))), ), - cel.CostEstimatorOptions( - checker.OverloadCostEstimate("regex_extract_string_string", estimateExtractCost()), - checker.OverloadCostEstimate("regex_extractAll_string_string", estimateExtractAllCost()), - checker.OverloadCostEstimate("regex_replace_string_string_string", estimateReplaceCost()), - checker.OverloadCostEstimate("regex_replace_string_string_string_int", estimateReplaceCost()), + cel.CostModel( + regexExtractCostModel("regex_extract_string_string"), + regexExtractAllCostModel("regex_extractAll_string_string"), + regexReplaceCostModel("regex_replace_string_string_string"), + regexReplaceCostModel("regex_replace_string_string_string_int"), ), cel.EnvOption(optionalTypesEnabled), } @@ -167,14 +165,7 @@ func (r *regexLib) CompileOptions() []cel.EnvOption { // ProgramOptions implements the cel.Library interface method func (r *regexLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{ - cel.CostTrackerOptions( - interpreter.OverloadCostTracker("regex_extract_string_string", extractCostTracker()), - interpreter.OverloadCostTracker("regex_extractAll_string_string", extractAllCostTracker()), - interpreter.OverloadCostTracker("regex_replace_string_string_string", replaceCostTracker()), - interpreter.OverloadCostTracker("regex_replace_string_string_string_int", replaceCostTracker()), - ), - } + return []cel.ProgramOption{} } func regReplace(args ...ref.Val) ref.Val { @@ -335,118 +326,108 @@ func extractAll(target, regexStr ref.Val) ref.Val { return types.NewStringList(types.DefaultTypeAdapter, result) } -func estimateExtractCost() checker.FunctionEstimator { - return func(c checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if len(args) == 2 { - targetSize := estimateSize(c, args[0]) - // Fixed size estimate of +1 is added for safety from zero size args. - // The target cost is the size of the target string, scaled by a traversal factor. - targetCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) - // The regex cost is the size of the regex pattern, scaled by a complexity factor. - regexCost := estimateSize(c, args[1]).Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) - // The result is a single string. Worst Case: it's the size of the entire target. - resultSize := rangedSizeEstimate(0, targetSize.Max) - // The total cost is the search cost (target + regex) plus the allocation cost for the result string. - return callEstimate( - regexCost.Multiply(targetCost).Add(checker.CostEstimate(resultSize)), - &resultSize, - ) - } +func estimateRegexSearchCost(c cost.Estimator, args []cost.AstNode) (cost.SizeEstimate, cost.CostEstimate) { + targetSize := cost.EstimateSize(c, args[0]) + // Fixed size estimate of +1 is added for safety from zero size args. + // The target cost is the size of the target string, scaled by a traversal factor. + targetCost := targetSize.Add(cost.FixedSizeEstimate(1)).MultiplyByCostFactor(cost.StringTraversalCostFactor) + // The regex cost is the size of the regex pattern, scaled by a complexity factor. + regexCost := cost.EstimateSize(c, args[1]).Add(cost.FixedSizeEstimate(1)).MultiplyByCostFactor(cost.RegexStringLengthCostFactor) + return targetSize, targetCost.Multiply(regexCost) +} + +func estimateExtractCost(c cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) != 2 { return nil } + targetSize, searchCost := estimateRegexSearchCost(c, args) + // The result is a single string. Worst Case: it's the size of the entire target. + resultSize := cost.RangedSizeEstimate(0, targetSize.Max) + // The total cost is the search cost plus the allocation cost for the result string. + return cost.NewCallEstimate(searchCost.Add(resultSize.AsCost()), &resultSize) } -func estimateExtractAllCost() checker.FunctionEstimator { - return func(c checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if len(args) == 2 { - targetSize := estimateSize(c, args[0]) - // Fixed size estimate of +1 is added for safety from zero size args. - // The target cost is the size of the target string, scaled by a traversal factor. - targetCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) - // The regex cost is the size of the regex pattern, scaled by a complexity factor. - regexCost := estimateSize(c, args[1]).Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) - // The result is a list of strings. Worst Case: it's contents are the size of the entire target. - resultSize := rangedSizeEstimate(0, targetSize.Max) - // The cost to allocate the result list is its base cost plus the size of its contents. - allocationSize := resultSize.Add(fixedSizeEstimate(common.ListCreateBaseCost)) - // The total cost is the search cost (target + regex) plus the allocation cost for the result list. - return callEstimate( - targetCost.Multiply(regexCost).Add(checker.CostEstimate(allocationSize)), - &resultSize, - ) - } +func estimateExtractAllCost(c cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if len(args) != 2 { return nil } + targetSize, searchCost := estimateRegexSearchCost(c, args) + // The result is a list of strings. Worst Case: its contents are the size of the entire target. + elemSize := cost.RangedSizeEstimate(0, targetSize.Max) + resultSize := cost.ListSizeEstimate(cost.RangedSizeEstimate(0, targetSize.Max), elemSize) + // The cost to allocate the result list is its base cost plus the size of its contents. + allocationSize := resultSize.Add(cost.FixedSizeEstimate(cost.ListCreateBaseCost)) + // The total cost is the search cost plus the allocation cost for the result list. + return cost.NewCallEstimate(searchCost.Add(allocationSize.AsCost()), &resultSize) } -func estimateReplaceCost() checker.FunctionEstimator { - return func(c checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - l := len(args) - if target == nil && (l == 3 || l == 4) { - targetSize := estimateSize(c, args[0]) - replacementSize := estimateSize(c, args[2]) - // Fixed size estimate of +1 is added for safety from zero size args. - // The target cost is the size of the target string, scaled by a traversal factor. - targetCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) - // The regex cost is the size of the regex pattern, scaled by a complexity factor. - regexCost := estimateSize(c, args[1]).Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) - // Estimate the potential size range of the output string. The final size could be smaller - // (if the replacement size is 0) or larger than the original. - allReplacedSize := targetSize.Max * replacementSize.Max - noneReplacedSize := targetSize.Max - // The allocation cost for the result is based on the estimated size of the output string. - resultSize := rangedSizeEstimate(noneReplacedSize, allReplacedSize) - if replacementSize.Max == 0 { - resultSize = rangedSizeEstimate(allReplacedSize, noneReplacedSize) - } - // The final cost is result of search cost (target cost + regex cost) plus the allocation cost for the output string. - return callEstimate( - targetCost.Multiply(regexCost).Add(resultSize.AsCost()), - &resultSize, - ) - } +func estimateReplaceCost(c cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { + if target != nil || (len(args) != 3 && len(args) != 4) { return nil } + targetSize, searchCost := estimateRegexSearchCost(c, args) + replacementSize := cost.EstimateSize(c, args[2]) + // Estimate the potential size range of the output string. The final size could be smaller + // (if the replacement size is 0) or larger than the original. + allReplacedSize := cost.SafeMultiply(targetSize.Max, replacementSize.Max) + noneReplacedSize := targetSize.Max + resultSize := cost.RangedSizeEstimate(noneReplacedSize, allReplacedSize) + if replacementSize.Max == 0 { + resultSize = cost.RangedSizeEstimate(allReplacedSize, noneReplacedSize) + } + // The final cost is the search cost plus the allocation cost for the output string. + return cost.NewCallEstimate(searchCost.Add(resultSize.AsCost()), &resultSize) } -func extractCostTracker() interpreter.FunctionTracker { - return func(args []ref.Val, result ref.Val) *uint64 { - targetCost := float64(safeAdd(actualSize(args[0]), 1)) * common.StringTraversalCostFactor - regexCost := float64(safeAdd(actualSize(args[1]), 1)) * common.RegexStringLengthCostFactor - // Actual search cost calculation = targetCost + regexCost - searchCost := targetCost * regexCost - // The total cost is the base call cost + search cost + result string allocation. - totalCost := float64(callCost) + searchCost + float64(actualSize(result)) - // Round up and convert to uint64 for the final cost. - finalCost := uint64(math.Ceil(totalCost)) - return &finalCost - } +func trackReplaceCost(args []ref.Val, result ref.Val) *uint64 { + targetCost := float64(cost.SafeAdd(cost.ActualSize(args[0]), 1)) * cost.StringTraversalCostFactor + regexCost := float64(cost.SafeAdd(cost.ActualSize(args[1]), 1)) * cost.RegexStringLengthCostFactor + totalCost := float64(cost.CallCost) + targetCost*regexCost + float64(cost.ActualSize(result)) + // Convert to uint64 for the final cost. Unlike the extract trackers this truncates; the + // two are reconciled in cost model version 1 to avoid raising the ModelVersion0 charge. + finalCost := cost.SafeTrunc(totalCost) + return &finalCost } -func extractAllCostTracker() interpreter.FunctionTracker { - return func(args []ref.Val, result ref.Val) *uint64 { - targetCost := float64(actualSize(args[0])+1) * common.StringTraversalCostFactor - regexCost := float64(actualSize(args[1])+1) * common.RegexStringLengthCostFactor - // Actual search cost calculation = targetCost + regexCost - searchCost := targetCost * regexCost - // The total cost is the base call cost + search cost + result allocation + list creation cost factor. - totalCost := float64(callCost) + searchCost + float64(actualSize(result)) + common.ListCreateBaseCost - // Round up and convert to uint64 for the final cost. - finalCost := uint64(math.Ceil(totalCost)) - return &finalCost - } +// regexSearch is the cost of matching pattern against target: the product of their lengths. +func regexSearch(target, pattern cost.QuantityExpr) cost.QuantityExpr { + return cost.Mul( + cost.Scale(cost.Sum(cost.Length(target), cost.Const(1)), cost.StringTraversalCostFactor), + cost.Scale(cost.Sum(cost.Length(pattern), cost.Const(1)), cost.RegexStringLengthCostFactor), + ) } -func replaceCostTracker() interpreter.FunctionTracker { - return func(args []ref.Val, result ref.Val) *uint64 { - targetCost := float64(actualSize(args[0])+1) * common.StringTraversalCostFactor - regexCost := float64(actualSize(args[1])+1) * common.RegexStringLengthCostFactor - // Actual search cost calculation = targetCost + regexCost - searchCost := targetCost * regexCost - // The total cost is the base call cost + search cost + result string allocation. - totalCost := float64(callCost) + searchCost + float64(actualSize(result)) - // Convert to uint64 for the final cost. - finalCost := uint64(totalCost) - return &finalCost - } +// regexExtractCostModel models regex.extract: it searches the target, and returns an optional +// holding the first match or capture group, a part of the target. The result is charged as the +// optional it is, 1 plus the size of the part (1 when nothing matched); see cost.Optional. Version 0 +// sizes an optional as 1, which is what v0.32.0 charged. +func regexExtractCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), regexSearch(cost.Arg(0), cost.Arg(1)), cost.Result())), + cost.ResultSize(cost.Optional(cost.AtMost(cost.Arg(0)))), + cost.BeforeVersion(1, cost.ManualEstimator(estimateExtractCost)), + ) } + +// regexExtractAllCostModel models regex.extractAll: it returns parts of the target, sized as the +// parts of a split. +func regexExtractAllCostModel(id string) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.AllocList(regexSearch(cost.Arg(0), cost.Arg(1)), cost.Length(cost.Result()))), + cost.ResultSize(splitSize(cost.Arg(0))), + cost.BeforeVersion(1, cost.ManualEstimator(estimateExtractAllCost)), + ) +} + +// regexReplaceCostModel models regex.replace: it may expand each of the at most n + 1 matches into +// the replacement, whose capture group references may each expand to the whole target. +func regexReplaceCostModel(id string) cost.OverloadModel { + target := cost.Arg(0) + return cost.Overload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), regexSearch(target, cost.Arg(1)), cost.Length(cost.Result()))), + cost.ResultSize(cost.AtMost(cost.Sum(target, + cost.Mul(cost.Sum(target, cost.Const(1)), cost.Arg(2), cost.Sum(target, cost.Const(1)))))), + cost.BeforeVersion(1, cost.ManualCost(estimateReplaceCost, trackReplaceCost)), + ) +} + diff --git a/vendor/github.com/google/cel-go/ext/sets.go b/vendor/cel.dev/cel-go/ext/sets.go similarity index 80% rename from vendor/github.com/google/cel-go/ext/sets.go rename to vendor/cel.dev/cel-go/ext/sets.go index 63c019ad9f7..831afac6711 100644 --- a/vendor/github.com/google/cel-go/ext/sets.go +++ b/vendor/cel.dev/cel-go/ext/sets.go @@ -15,14 +15,13 @@ package ext import ( - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // Sets returns a cel.EnvOption to configure namespaced set relationship @@ -116,24 +115,17 @@ func (setsLib) CompileOptions() []cel.EnvOption { cel.Function("sets.intersects", cel.Overload("list_sets_intersects_list", []*cel.Type{listType, listType}, cel.BoolType, cel.BinaryBinding(setsIntersects))), - cel.CostEstimatorOptions( - checker.OverloadCostEstimate("list_sets_contains_list", estimateSetsCost(1)), - checker.OverloadCostEstimate("list_sets_intersects_list", estimateSetsCost(1)), - // equivalence requires potentially two m*n comparisons to ensure each list is contained by the other - checker.OverloadCostEstimate("list_sets_equivalent_list", estimateSetsCost(2)), + cel.CostModel( + setsCostModel("list_sets_contains_list", 1), + setsCostModel("list_sets_equivalent_list", 2), + setsCostModel("list_sets_intersects_list", 1), ), } } // ProgramOptions implements the Library interface method. func (setsLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{ - cel.CostTrackerOptions( - interpreter.OverloadCostTracker("list_sets_contains_list", trackSetsCost(1)), - interpreter.OverloadCostTracker("list_sets_intersects_list", trackSetsCost(1)), - interpreter.OverloadCostTracker("list_sets_equivalent_list", trackSetsCost(2)), - ), - } + return []cel.ProgramOption{} } // NewSetMembershipOptimizer rewrites set membership tests using the `in` operator against a list @@ -232,23 +224,33 @@ func setsEquivalent(listA, listB ref.Val) ref.Val { return setsContains(listB, listA) } -func estimateSetsCost(costFactor float64) checker.FunctionEstimator { - return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { +func setsCostModel(id string, costFactor float64) cost.OverloadModel { + return cost.Overload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), + cost.Scale(cost.Mul(cost.Length(cost.Arg(0)), cost.Length(cost.Arg(1))), costFactor))), + cost.BeforeVersion(1, cost.ManualCost(estimateSetsCost(costFactor), trackSetsCost(costFactor)))) +} + +func estimateSetsCost(costFactor float64) cost.FunctionEstimator { + return func(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { if len(args) != 2 { return nil } - arg0Size := estimateSize(estimator, args[0]) - arg1Size := estimateSize(estimator, args[1]) - costEstimate := arg0Size.Multiply(arg1Size).MultiplyByCostFactor(costFactor).Add(callCostEstimate) - return callEstimate(costEstimate, nil) + arg0Size := cost.EstimateSize(estimator, args[0]) + arg1Size := cost.EstimateSize(estimator, args[1]) + costEstimate := arg0Size.Multiply(arg1Size).MultiplyByCostFactor(costFactor).Add(cost.CallCostEstimate) + return cost.NewCallEstimate(costEstimate, nil) } } -func trackSetsCost(costFactor float64) interpreter.FunctionTracker { +func trackSetsCost(costFactor float64) cost.FunctionTracker { return func(args []ref.Val, _ ref.Val) *uint64 { - lhsSize := actualSize(args[0]) - rhsSize := actualSize(args[1]) - cost := safeAdd(callCost, uint64(float64(lhsSize*rhsSize)*costFactor)) - return &cost + lhsSize := cost.ActualSize(args[0]) + rhsSize := cost.ActualSize(args[1]) + // The pairwise comparison count saturates rather than wrapping, and the scaled result + // avoids a platform-defined out-of-range float conversion. + comparisons := cost.SafeMultiply(lhsSize, rhsSize) + total := cost.SafeAdd(cost.CallCost, cost.SafeMultiplyByFactorTrunc(comparisons, costFactor)) + return &total } } diff --git a/vendor/github.com/google/cel-go/ext/strings.go b/vendor/cel.dev/cel-go/ext/strings.go similarity index 71% rename from vendor/github.com/google/cel-go/ext/strings.go rename to vendor/cel.dev/cel-go/ext/strings.go index 1f7732f2fb4..d2c71b4c140 100644 --- a/vendor/github.com/google/cel-go/ext/strings.go +++ b/vendor/cel.dev/cel-go/ext/strings.go @@ -27,13 +27,11 @@ import ( "golang.org/x/text/language" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) const ( @@ -592,57 +590,33 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption { } if lib.version >= 5 { - // Cost estimators for string extension functions. - estimators := []checker.CostOption{ - // Format is captured in the core cost estimator logic and needs to be extracted out. - checker.OverloadCostEstimate("string_char_at_int", estimateStringCharAtCost), - checker.OverloadCostEstimate("string_index_of_string", estimateStringSearchCost), - checker.OverloadCostEstimate("string_index_of_string_int", estimateStringSearchCost), - checker.OverloadCostEstimate("string_last_index_of_string", estimateStringSearchCost), - checker.OverloadCostEstimate("string_last_index_of_string_int", estimateStringSearchCost), - checker.OverloadCostEstimate("string_lower_ascii", estimateStringFixedTransformCost), - checker.OverloadCostEstimate("string_upper_ascii", estimateStringFixedTransformCost), - checker.OverloadCostEstimate("string_replace_string_string", estimateStringReplaceCost), - checker.OverloadCostEstimate("string_replace_string_string_int", estimateStringReplaceCost), - checker.OverloadCostEstimate("string_split_string", estimateStringSplitCost), - checker.OverloadCostEstimate("string_split_string_int", estimateStringSplitCost), - checker.OverloadCostEstimate("string_substring_int", estimateSubstringCost), - checker.OverloadCostEstimate("string_substring_int_int", estimateSubstringCost), - checker.OverloadCostEstimate("string_trim", estimateStringVariableTransformCost), - checker.OverloadCostEstimate("string_reverse", estimateStringFixedTransformCost), - checker.OverloadCostEstimate("list_join", estimateStringJoinCost), - checker.OverloadCostEstimate("list_join_string", estimateStringJoinCost), - } - opts = append(opts, cel.CostEstimatorOptions(estimators...)) + opts = append(opts, + cel.CostModel( + stringCharAtCostModel("string_char_at_int"), + stringSearchCostModel("string_index_of_string"), + stringSearchCostModel("string_index_of_string_int"), + stringSearchCostModel("string_last_index_of_string"), + stringSearchCostModel("string_last_index_of_string_int"), + stringFixedTransformCostModel("string_lower_ascii"), + stringFixedTransformCostModel("string_upper_ascii"), + stringFixedTransformCostModel("string_reverse"), + stringTrimCostModel("string_trim"), + stringSubstringCostModel("string_substring_int"), + stringSubstringRangeCostModel("string_substring_int_int"), + listJoinCostModel("list_join"), + listJoinStringCostModel("list_join_string"), + stringReplaceCostModel("string_replace_string_string"), + stringReplaceCostModel("string_replace_string_string_int"), + stringSplitCostModel("string_split_string"), + stringSplitCostModel("string_split_string_int"), + ), + ) } return opts } // ProgramOptions implements the Library interface method. func (lib *stringLib) ProgramOptions() []cel.ProgramOption { - if lib.version >= 5 { - return []cel.ProgramOption{ - cel.CostTrackerOptions( - interpreter.OverloadCostTracker("string_char_at_int", trackStringCharAtCost), - interpreter.OverloadCostTracker("string_index_of_string", trackStringSearchCost), - interpreter.OverloadCostTracker("string_index_of_string_int", trackStringSearchCost), - interpreter.OverloadCostTracker("string_last_index_of_string", trackStringSearchCost), - interpreter.OverloadCostTracker("string_last_index_of_string_int", trackStringSearchCost), - interpreter.OverloadCostTracker("string_lower_ascii", trackStringTransformCost), - interpreter.OverloadCostTracker("string_upper_ascii", trackStringTransformCost), - interpreter.OverloadCostTracker("string_replace_string_string", trackStringReplaceCost), - interpreter.OverloadCostTracker("string_replace_string_string_int", trackStringReplaceCost), - interpreter.OverloadCostTracker("string_split_string", trackStringSplitCost), - interpreter.OverloadCostTracker("string_split_string_int", trackStringSplitCost), - interpreter.OverloadCostTracker("string_substring_int", trackStringTransformCost), - interpreter.OverloadCostTracker("string_substring_int_int", trackStringTransformCost), - interpreter.OverloadCostTracker("string_trim", trackStringTransformCost), - interpreter.OverloadCostTracker("string_reverse", trackStringTransformCost), - interpreter.OverloadCostTracker("list_join", trackStringJoinCost), - interpreter.OverloadCostTracker("list_join_string", trackStringJoinCost), - ), - } - } return []cel.ProgramOption{} } @@ -898,187 +872,157 @@ var ( // the input string(s), ensuring that the CEL cost system accurately reflects the // computational work performed by string operations. -// estimateStringFixedTransformCost estimates cost for O(n) string operations such as -// lowerAscii, upperAsciil, reverse and quote. -func estimateStringFixedTransformCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil { - return nil - } - cost, size := estimateStringScan(estimateSize(estimator, *target)) - return callEstimate(cost.Add(callCostEstimate).Add(size.AsCost()), size) -} - -// estimateStringVariableTransformCost estimates cost for O(n) string operations that result -// in a variable sized string which may be empty to the exact input string. -func estimateStringVariableTransformCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil { - return nil - } - cost, size := estimateStringScan(estimateSize(estimator, *target)) - transformSize := rangedSizeEstimate(0, size.Max) - return callEstimate(cost.Add(callCostEstimate).Add(transformSize.AsCost()), &transformSize) -} - -// estimateStringCharAtCost includes a cost of 1 for the allocation, plus the string traversal cost. -func estimateStringCharAtCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) != 1 { - return nil - } - cost, _ := estimateStringScan(estimateSize(estimator, *target)) - resultSize := rangedSizeEstimate(0, 1) - return callEstimate(cost.Add(callCostEstimate).Add(callCostEstimate), &resultSize) -} - // estimateSubstringCost estimates the cost for an O(n) traversal and allocation. -func estimateSubstringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { +func estimateSubstringCost(estimator cost.Estimator, target *cost.AstNode, args []cost.AstNode) *cost.CallEstimate { if target == nil || len(args) < 1 || len(args) > 2 { return nil } - targetSize := estimateSize(estimator, *target) - cost, _ := estimateStringScan(targetSize) + targetSize := cost.EstimateSize(estimator, *target) + evalCost, _ := cost.EstimateStringScan(targetSize) - start := nodeAsUintValue(args[0], 0) + start := cost.NodeAsUintValue(args[0], 0) end := targetSize.Max if len(args) == 2 { - end = nodeAsUintValue(args[1], end) + end = cost.NodeAsUintValue(args[1], end) } - resultSize := fixedSizeEstimate(end - start) - return callEstimate(cost.Add(callCostEstimate).Add(resultSize.AsCost()), &resultSize) + resultSize := cost.FixedSizeEstimate(end - start) + return cost.NewCallEstimate(evalCost.Add(cost.CallCostEstimate).Add(resultSize.AsCost()), &resultSize) } -// estimateStringSearchCost estimates cost for O(n*m) string search operations -// such as indexOf and lastIndexOf. -func estimateStringSearchCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) < 1 { - return nil - } - targetSize := estimateSize(estimator, *target) - needleSize := estimateSize(estimator, args[0]) - searchSize := targetSize.Multiply(needleSize) - searchCost, _ := estimateStringScan(searchSize) - // Search cost is proportional to target size * substring size. - return callEstimate(searchCost.Add(callCostEstimate), nil) -} - -// estimateStringReplaceCost estimates cost for string replace operations. -// The cost accounts for search (O(n*m)) and potential output size growth. -func estimateStringReplaceCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) < 2 { - return nil - } - // Compute the search for the replacement string, by 'm' times - targetSize := estimateSize(estimator, *target) - needleSize := atLeastOne(estimateSize(estimator, args[0])) - searchCost := atLeastOne(targetSize).Multiply(needleSize).MultiplyByCostFactor(stringCostFactor) - - replacementSize := estimateSize(estimator, args[1]).Add(fixedSizeEstimate(1)) - allReplacedSize := safeMul(safeAdd(targetSize.Max, 1), replacementSize.Max) - resultMinSize := targetSize.Min - if resultMinSize > replacementSize.Min { - resultMinSize = replacementSize.Min - } - resultSize := rangedSizeEstimate(resultMinSize, allReplacedSize) - return callEstimate( - searchCost.Add(resultSize.AsCost()).Add(callCostEstimate), &resultSize, +// splitSize bounds the size of the parts of a string of size n: at most n + 1 parts of at most n +// each. Under the aggregate strategy the parts hold at most the string's content plus one unit +// for each part, an empty string having size 1, so the list's size is at most 2n + 2, which also +// bounds the default strategy's n + 1 parts. +func splitSize(s cost.QuantityExpr) cost.QuantityExpr { + return cost.Min( + cost.List(cost.AtMost(cost.Sum(s, cost.Const(1))), cost.AtMost(s)), + cost.Sum(cost.Scale(s, 2), cost.Const(2)), ) } -// estimateStringSplitCost estimates cost for string split operations. -// Split creates a list of substrings, so cost includes both traversal and -// list allocation proportional to the input size. -func estimateStringSplitCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) < 1 { - return nil - } - targetSize := estimateSize(estimator, *target) - // Traversal cost proportional to input size. - traversalCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(stringCostFactor) - // Worst case: split("") produces N elements for a string of size N. - resultSize := rangedSizeEstimate(0, targetSize.Max) - // Include list creation base cost plus allocation for each element. - allocationCost := resultSize.MultiplyByCostFactor(1).Add(checker.FixedCostEstimate(common.ListCreateBaseCost)) - cost := traversalCost.Add(allocationCost).Add(callCostEstimate) - return callEstimate(cost, &resultSize) +// stringTransformEvalCost is the EvalCost of the O(n) string transforms: a scan of the receiver +// and the result's length. +func stringTransformEvalCost() cost.OverloadModelOption { + return cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.StringScan(cost.Length(cost.Target())), cost.Length(cost.Result()))) } -// estimateStringJoinCost estimates cost for string join operations. -// Join iterates over all list elements and concatenates them, so cost is -// proportional to the total size of all elements plus separator overhead. -func estimateStringJoinCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil { - return nil - } - targetSize := estimateSize(estimator, *target) - sepSize := fixedSizeEstimate(0) - if len(args) >= 1 { - sepSize = estimateSize(estimator, args[0]) - } - // Traversal cost proportional to the number of list elements. - traversalCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(stringCostFactor) - // Result size: sum of element sizes + (n-1) * separator size. - // Worst case estimate: use list size * max element size + list size * separator size. - maxResultSize := safeAdd(safeMul(targetSize.Max, (safeAdd(1, sepSize.Max))), sepSize.Max) - resultSize := rangedSizeEstimate(0, maxResultSize) - cost := traversalCost.Add(resultSize.MultiplyByCostFactor(1)).Add(callCostEstimate) - return callEstimate(cost, &resultSize) +// stringCharAtCostModel models charAt: it scans the receiver and returns a single code point, which +// is at most 4 UTF-8 bytes under the aggregate strategy and never above the receiver's size. +func stringCharAtCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost+1), cost.StringScan(cost.Length(cost.Target())))), + cost.ResultSize(cost.AtMost(cost.Min(cost.Target(), cost.Const(4)))), + cost.BeforeVersion(1, cost.ResultSize(cost.AtMost(cost.Const(1)))), + ) } -// Runtime cost tracking functions for string extensions. -// -// These functions compute the actual cost of string operations after evaluation, -// using the real sizes of the inputs and outputs. - -// trackStringCharAtCost tracks runtime cost for O(n) string operations. -func trackStringCharAtCost(args []ref.Val, result ref.Val) *uint64 { - size := float64(actualSize(args[0])) * stringCostFactor - cost := safeAdd(callCost, uint64(math.Ceil(size)), 1) - return &cost +func stringSearchCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.StringScan(cost.Mul(cost.Length(cost.Target()), cost.Length(cost.Arg(0)))))), + ) } -// trackStringTransformCost tracks runtime cost for O(n) string operations. -func trackStringTransformCost(args []ref.Val, result ref.Val) *uint64 { - transformCost := math.Ceil(float64(actualSize(args[0])) * stringCostFactor) - resultSize := actualSize(result) - cost := safeAdd(callCost, uint64(transformCost), resultSize) - return &cost +func stringFixedTransformCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + stringTransformEvalCost(), + cost.ResultSize(cost.Target()), + ) } -// trackStringSearchCost tracks runtime cost for O(n*m) string search operations. -func trackStringSearchCost(args []ref.Val, _ ref.Val) *uint64 { - searchCost := float64(actualSize(args[0])*actualSize(args[1])) * stringCostFactor - cost := safeAdd(uint64(math.Ceil(searchCost)), callCost) - return &cost +func stringTrimCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + stringTransformEvalCost(), + cost.ResultSize(cost.AtMost(cost.Target())), + ) } -// trackStringReplaceCost tracks runtime cost for string replace operations, -// accounting for search cost and the size of the result. -func trackStringReplaceCost(args []ref.Val, result ref.Val) *uint64 { - targetSize := actualSize(args[0]) - if targetSize == 0 { - targetSize = 1 - } - needleSize := actualSize(args[1]) - if needleSize == 0 { - needleSize = 1 - } - searchCost := uint64(math.Ceil(float64(targetSize*needleSize) * stringCostFactor)) - cost := safeAdd(callCost, searchCost, actualSize(result)) - return &cost +// stringSubstringCostModel models s.substring(start): it drops at least start units of the +// receiver, and at most 4 times as many under the aggregate strategy (where each code point takes +// 1 to 4 UTF-8 bytes). +func stringSubstringCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + stringTransformEvalCost(), + cost.ResultSize(cost.Ranged( + cost.Sub(cost.Target(), cost.Scale(cost.IntArg(0, 0), 4)), + cost.Sub(cost.Target(), cost.IntArg(0, 0)), + )), + cost.BeforeVersion(1, cost.ManualEstimator(estimateSubstringCost)), + ) } -// trackStringSplitCost tracks runtime cost for string split operations, -// accounting for traversal and list allocation. -func trackStringSplitCost(args []ref.Val, result ref.Val) *uint64 { - traversalCost := float64(safeAdd(actualSize(args[0]), 1)) * stringCostFactor - resultSize := actualSize(result) - cost := safeAdd(callCost, uint64(math.Ceil(traversalCost)), resultSize, common.ListCreateBaseCost) - return &cost +// stringSubstringRangeCostModel models s.substring(start, end): the result has end - start code +// points, sized between that length and 4 times it under the aggregate strategy, and never above +// the remaining suffix of the receiver. +func stringSubstringRangeCostModel(id string) cost.OverloadModel { + length := cost.Sub(cost.IntArg(1, 0), cost.IntArg(0, 0)) + return cost.MemberOverload(id, + stringTransformEvalCost(), + cost.ResultSize(cost.Ranged( + length, + cost.Min(cost.Sub(cost.Target(), cost.IntArg(0, 0)), cost.Scale(length, 4)), + )), + cost.BeforeVersion(1, cost.ManualEstimator(estimateSubstringCost)), + ) } -// trackStringJoinCost tracks runtime cost for string join operations, -// accounting for traversal and the size of the result. -func trackStringJoinCost(args []ref.Val, result ref.Val) *uint64 { - traversalCost := float64(safeAdd(actualSize(args[0]), 1)) * stringCostFactor - cost := safeAdd(callCost, uint64(math.Ceil(traversalCost)), actualSize(result)) - return &cost +// listJoinCostModel models l.join(): it reads the list once and builds the concatenated result. +func listJoinCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.StringScan(cost.Sum(cost.Length(cost.Target()), cost.Const(1))), cost.Length(cost.Result()))), + cost.ResultSize(cost.AtMost(cost.ElemTotal(cost.Target()))), + cost.BeforeVersion(1, cost.ResultSize(cost.AtMost(cost.Length(cost.Target())))), + ) } + +// listJoinStringCostModel models l.join(delim): it reads the list once and builds the result with +// delimiters between elements. +func listJoinStringCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum(cost.Const(cost.CallCost), cost.StringScan(cost.Sum(cost.Length(cost.Target()), cost.Const(1))), cost.Length(cost.Result()))), + cost.ResultSize(cost.AtMost(cost.Sum( + cost.ElemTotal(cost.Target()), + cost.Mul(cost.Length(cost.Target()), cost.Arg(0)), + ))), + cost.BeforeVersion(1, cost.ResultSize(cost.AtMost(cost.Sum( + cost.Mul(cost.Length(cost.Target()), cost.Sum(cost.Const(1), cost.Arg(0))), + cost.Arg(0), + )))), + ) +} + +// stringReplaceCostModel models replace: it searches the receiver for the needle and builds the +// result. Without a match the result is the receiver, and with one it contains the replacement, so +// it is at least the smaller of the two. Each of the at most n + 1 matches adds at most the +// replacement. +func stringReplaceCostModel(id string) cost.OverloadModel { + one := cost.Const(1) + maxSize := cost.Mul(cost.Sum(cost.Target(), one), cost.Sum(cost.Arg(1), one)) + return cost.MemberOverload(id, + cost.EvalCost(cost.Sum( + cost.Const(cost.CallCost), + cost.StringScan(cost.Mul( + cost.AtLeastOneQuantity(cost.Length(cost.Target())), + cost.AtLeastOneQuantity(cost.Length(cost.Arg(0))), + )), + cost.Length(cost.Result()), + )), + cost.ResultSize(cost.Ranged(cost.Min(cost.Target(), cost.Arg(1)), maxSize)), + cost.BeforeVersion(1, cost.ResultSize(cost.Ranged( + cost.Min(cost.Target(), cost.Sum(cost.Arg(1), one)), + maxSize, + ))), + ) +} + +// stringSplitCostModel models split: it scans the receiver and allocates the parts. +func stringSplitCostModel(id string) cost.OverloadModel { + return cost.MemberOverload(id, + cost.EvalCost(cost.AllocList( + cost.StringScan(cost.Sum(cost.Length(cost.Target()), cost.Const(1))), + cost.Length(cost.Result()), + )), + cost.ResultSize(splitSize(cost.Target())), + cost.BeforeVersion(1, cost.ResultSize(cost.List(cost.AtMost(cost.Target()), cost.AtMost(cost.Target())))), + ) +} + diff --git a/vendor/github.com/google/cel-go/interpreter/BUILD.bazel b/vendor/cel.dev/cel-go/interpreter/BUILD.bazel similarity index 90% rename from vendor/github.com/google/cel-go/interpreter/BUILD.bazel rename to vendor/cel.dev/cel-go/interpreter/BUILD.bazel index 1274a6f12da..a1f99a85a83 100644 --- a/vendor/github.com/google/cel-go/interpreter/BUILD.bazel +++ b/vendor/cel.dev/cel-go/interpreter/BUILD.bazel @@ -9,6 +9,7 @@ go_library( name = "go_default_library", srcs = [ "activation.go", + "async.go", "attribute_patterns.go", "attributes.go", "decorators.go", @@ -21,12 +22,14 @@ go_library( "planner.go", "prune.go", "runtimecost.go", + "runtimememory.go", ], - importpath = "github.com/google/cel-go/interpreter", + importpath = "cel.dev/cel-go/interpreter", deps = [ "//common:go_default_library", "//common/ast:go_default_library", "//common/containers:go_default_library", + "//common/cost:go_default_library", "//common/functions:go_default_library", "//common/operators:go_default_library", "//common/overloads:go_default_library", @@ -46,12 +49,15 @@ go_test( name = "go_default_test", srcs = [ "activation_test.go", + "async_test.go", "attribute_patterns_test.go", "attributes_test.go", "frame_test.go", "interpreter_test.go", + "mutability_test.go", "prune_test.go", "runtimecost_test.go", + "runtimememory_test.go", ], embed = [ ":go_default_library", @@ -65,6 +71,7 @@ go_test( "//common/operators:go_default_library", "//common/stdlib:go_default_library", "//common/types:go_default_library", + "//common/types/ref:go_default_library", "//parser:go_default_library", "//test:go_default_library", "//test/proto2pb:go_default_library", diff --git a/vendor/cel.dev/cel-go/interpreter/activation.go b/vendor/cel.dev/cel-go/interpreter/activation.go new file mode 100644 index 00000000000..9d2c20404b6 --- /dev/null +++ b/vendor/cel.dev/cel-go/interpreter/activation.go @@ -0,0 +1,385 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interpreter + +import ( + "errors" + "fmt" + + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/types/ref" +) + +// Activation used to resolve identifiers by name and references by id. +// +// An Activation is the primary mechanism by which a caller supplies input into a CEL program. +type Activation interface { + // ResolveName returns a value from the activation by qualified name, or false if the name + // could not be found. + ResolveName(name string) (any, bool) + + // Parent returns the parent of the current activation, may be nil. + // If non-nil, the parent will be searched during resolve calls. + Parent() Activation +} + +// EmptyActivation returns a variable-free activation. +func EmptyActivation() Activation { + return emptyActivation{} +} + +// emptyActivation is a variable-free activation. +type emptyActivation struct{} + +func (emptyActivation) ResolveName(string) (any, bool) { return nil, false } + +func (emptyActivation) Parent() Activation { return nil } + +// NewActivation returns an activation based on a map-based binding where the map keys are +// expected to be qualified names used with ResolveName calls. +// +// The input `bindings` may either be of type `Activation` or `map[string]any`. +// +// Lazy bindings may be supplied within the map-based input in either of the following forms: +// - func() any +// - func() ref.Val +// +// The output of the lazy binding will overwrite the variable reference in the internal map. +// +// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using +// the types.Adapter configured in the environment. +func NewActivation(bindings any) (Activation, error) { + if bindings == nil { + return nil, errors.New("bindings must be non-nil") + } + a, isActivation := bindings.(Activation) + if isActivation { + return a, nil + } + m, isMap := bindings.(map[string]any) + if !isMap { + return nil, fmt.Errorf( + "activation input must be an activation or map[string]interface: got %T", + bindings) + } + return &mapActivation{bindings: m}, nil +} + +// mapActivation is the default Activation implementation, which supplies variables by name and, +// where the caller provided them, the implementations of late-bound functions. +// +// Variables and functions occupy separate namespaces, so the same qualified name may refer to +// both. An mapActivation may also delegate to the mapActivation whose bindings it extends, which is +// exposed as its parent so that the hierarchy is visible to all mapActivation traversals. +// +// Named bindings may lazily supply values by providing a function which accepts no arguments and +// produces an interface value. +type mapActivation struct { + bindings map[string]any + // TODO: validate that the late-bound functions should be applied here if at all. + // Seems like it would be better if populated via setter? + functions map[string]functions.LateBoundOp + parent Activation +} + +// Parent implements the Activation interface method. +func (a *mapActivation) Parent() Activation { + return a.parent +} + +// ResolveName implements the Activation interface method. +func (a *mapActivation) ResolveName(name string) (any, bool) { + obj, found := a.bindings[name] + if !found { + if a.parent != nil { + return a.parent.ResolveName(name) + } + return nil, false + } + fn, isLazy := obj.(func() ref.Val) + if isLazy { + obj = fn() + a.bindings[name] = obj + } + fnRaw, isLazy := obj.(func() any) + if isLazy { + obj = fnRaw() + a.bindings[name] = obj + } + return obj, found +} + +// ResolveFunction implements the FunctionActivation interface method. +// +// Only the bindings supplied to this activation are considered. A hierarchy may contain at most +// one activation which supplies late-bound functions, so there is no parent to delegate to. +func (a *mapActivation) ResolveFunction(name string) (functions.LateBoundOp, bool) { + fn, found := a.functions[name] + return fn, found +} + +// AsPartialActivation supports partial evaluation over the activation being extended. +func (a *mapActivation) AsPartialActivation() (PartialActivation, bool) { + return AsPartialActivation(a.parent) +} + +// hierarchicalActivation which implements Activation and contains a parent and +// child activation. +type hierarchicalActivation struct { + parent Activation + child Activation + poolAllocated bool +} + +// Parent implements the Activation interface method. +func (a *hierarchicalActivation) Parent() Activation { + return a.parent +} + +// ResolveName implements the Activation interface method. +func (a *hierarchicalActivation) ResolveName(name string) (any, bool) { + if a.child != nil { + if object, found := a.child.ResolveName(name); found { + return object, found + } + } + if a.parent != nil { + return a.parent.ResolveName(name) + } + return nil, false +} + +// 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 +} + +// IsLocalVariable reports whether the variable name is locally bound in the hierarchical activation. +func (a *hierarchicalActivation) IsLocalVariable(name string) bool { + if holder, ok := a.child.(localVariableHolder); ok { + if holder.IsLocalVariable(name) { + return true + } + } + if holder, ok := a.parent.(localVariableHolder); ok { + return holder.IsLocalVariable(name) + } + return false +} + +// 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: parent, child: child, poolAllocated: false} +} + +// NewPartialActivation returns an Activation which contains a list of AttributePattern values +// representing field and index operations that should result in a 'types.Unknown' result. +// +// The `bindings` value may be any value type supported by the interpreter.NewActivation call, +// but is typically either an existing Activation or map[string]any. +func NewPartialActivation(bindings any, + unknowns ...*AttributePattern) (PartialActivation, error) { + a, err := NewActivation(bindings) + if err != nil { + return nil, err + } + return &partActivation{Activation: a, unknowns: unknowns}, nil +} + +// PartialActivation extends the Activation interface with a set of UnknownAttributePatterns. +type PartialActivation interface { + Activation + + // UnknownAttributePaths returns a set of AttributePattern values which match Attribute + // expressions for data accesses whose values are not yet known. + UnknownAttributePatterns() []*AttributePattern +} + +// partialActivationConverter indicates whether an Activation implementation supports conversion to a PartialActivation +type partialActivationConverter interface { + // AsPartialActivation converts the current activation to a PartialActivation + AsPartialActivation() (PartialActivation, bool) +} + +// partActivation is the default implementations of the PartialActivation interface. +type partActivation struct { + Activation + unknowns []*AttributePattern +} + +// UnknownAttributePatterns implements the PartialActivation interface method. +func (a *partActivation) UnknownAttributePatterns() []*AttributePattern { + return a.unknowns +} + +// AsPartialActivation returns the partActivation as a PartialActivation interface. +func (a *partActivation) AsPartialActivation() (PartialActivation, bool) { + return a, true +} + +// AsPartialActivation walks the activation hierarchy and returns the first PartialActivation, if found. +func AsPartialActivation(vars Activation) (PartialActivation, bool) { + if vars == nil { + return nil, false + } + // Only internal activation instances may implement this interface + if pv, ok := vars.(partialActivationConverter); ok { + return pv.AsPartialActivation() + } + // Since Activations may be hierarchical, test whether a parent converts to a PartialActivation + if vars.Parent() != nil { + return AsPartialActivation(vars.Parent()) + } + return nil, false +} + +// FunctionActivation extends the Activation interface with late-bound function implementations. +// +// Function bindings occupy a namespace which is separate from variable bindings, so a function +// and a variable may share the same qualified name without ambiguity. +// +// The implementation which applies to an evaluation is located once, before evaluation begins, so +// at most one activation in a hierarchy may implement this interface. An implementation supplied +// by a caller is treated as the sole source of bindings and its parents are not searched. +type FunctionActivation interface { + Activation + + // ResolveFunction returns the implementation of the late-bound function by qualified name, + // or false if the function could not be found. + ResolveFunction(name string) (functions.LateBoundOp, bool) +} + +// NewFunctionActivation returns an Activation which supplies implementations for late-bound +// functions in addition to the variables provided by the `vars` input. +// +// The `vars` value may either be an Activation or any valid input to the NewActivation call, and +// the result is the same kind of activation that NewActivation produces, extended with the +// function bindings. +// +// Each function implementation is invoked with the id of the declared overload which matched the +// call arguments, which permits a single implementation to serve all overloads of the function. +// +// Late-bound functions are bound for the whole of an evaluation rather than for a lexical scope, +// so an error is returned when `vars` already supplies function bindings. Supply the complete set +// of bindings in a single call instead of layering them. +func NewFunctionActivation(vars any, funcs map[string]functions.LateBoundOp) (FunctionActivation, error) { + // Copy the input to ensure the bindings observed during an evaluation cannot be mutated by + // the caller once the activation has been created. + bindings := make(map[string]functions.LateBoundOp, len(funcs)) + for name, fn := range funcs { + if fn == nil { + return nil, fmt.Errorf("function binding must be non-nil: %s", name) + } + bindings[name] = fn + } + if m, isMap := vars.(map[string]any); isMap { + return &mapActivation{bindings: m, functions: bindings}, nil + } + a, err := NewActivation(vars) + if err != nil { + return nil, err + } + if err := checkNoFunctionActivation(a); err != nil { + return nil, err + } + return &mapActivation{functions: bindings, parent: a}, nil +} + +// errNestedFunctionActivation reports a hierarchy which supplies late-bound functions from more +// than one activation, where the bindings which apply would depend on the order in which the +// activations were composed. +var errNestedFunctionActivation = errors.New( + "activation hierarchy may supply late-bound functions from only one activation") + +// checkNoFunctionActivation returns an error if the hierarchy already supplies late-bound +// function implementations. +func checkNoFunctionActivation(vars Activation) error { + found, err := FindFunctionActivation(vars) + if err != nil { + return err + } + if found != nil { + return errNestedFunctionActivation + } + return nil +} + +// FindFunctionActivation returns the activation which supplies the late-bound function +// implementations for a hierarchy, or nil when the hierarchy supplies none. +// +// Late-bound functions are bound for the duration of an evaluation rather than for a lexical +// scope, so a hierarchy may contain at most one activation which supplies them and an error is +// returned when it contains more. Resolving the binding once, before evaluation begins, keeps the +// lookup at a call site independent of how many scopes enclose it. +// +// This must be called before evaluation begins, while the hierarchy is still composed only of +// caller-supplied activations. Scopes introduced during evaluation, such as those of a +// comprehension, report the enclosing frame as their parent rather than the enclosing activation. +func FindFunctionActivation(vars Activation) (FunctionActivation, error) { + return findFunctionActivation(vars, nil) +} + +// findFunctionActivation searches a hierarchy for the activation which supplies late-bound +// functions, carrying the match found so far so that a second match can be reported as an error. +func findFunctionActivation(vars Activation, found FunctionActivation) (FunctionActivation, error) { + if vars == nil { + return found, nil + } + switch a := vars.(type) { + case *mapActivation: + // A default activation only supplies functions when it was created for that purpose. + if a.functions != nil { + if found != nil { + return nil, errNestedFunctionActivation + } + found = a + } + return findFunctionActivation(a.parent, found) + case *hierarchicalActivation: + // Both scopes are caller-supplied, so both must be searched. + found, err := findFunctionActivation(a.parent, found) + if err != nil { + return nil, err + } + return findFunctionActivation(a.child, found) + case *partActivation: + // The wrapped activation is not reachable via Parent. + return findFunctionActivation(a.Activation, found) + default: + // An implementation supplied by the caller is responsible for the scopes it contains, + // so its parents are not searched. + if fa, isFunc := vars.(FunctionActivation); isFunc { + if found != nil { + return nil, errNestedFunctionActivation + } + return fa, nil + } + return findFunctionActivation(vars.Parent(), found) + } +} diff --git a/vendor/cel.dev/cel-go/interpreter/async.go b/vendor/cel.dev/cel-go/interpreter/async.go new file mode 100644 index 00000000000..2c40e516edb --- /dev/null +++ b/vendor/cel.dev/cel-go/interpreter/async.go @@ -0,0 +1,535 @@ +// 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" + "fmt" + "math" + "sync" + "sync/atomic" + + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// Async extension function support. +// +// CEL supports `types.Unknown` as a first-class value, and concurrent (async) function execution +// in CEL invokes a stub function which checks for the presence of an existing result which matches +// the function call and call arguments, or which records the 'unexecuted' function and call arguments +// for concurrent execution in a later phase if the result is `types.Unknown` and indicates the +// expression ids of the functions necessary to advance the execution. +// +// This call pattern is repeated iteratively until there are either no more functions to call or no +// progress is made toward resolving the unknowns. + +// AsyncObserver provides callbacks for monitoring the lifecycle of asynchronous function calls. +// +// Implementations must be safe for concurrent use: OnCallStarted is invoked from the evaluator +// goroutine when a call is launched, while OnCallFinished is invoked from the call's own goroutine +// when it completes. The two callbacks therefore run on different goroutines, and OnCallFinished +// callbacks for distinct calls may run concurrently with each other. +type AsyncObserver interface { + // OnCallStarted is called when an asynchronous function is first launched. + OnCallStarted(callID int64, function, overload string, args []ref.Val) + // OnCallFinished is called when an asynchronous function completes. + OnCallFinished(callID int64, function, overload string, res ref.Val) +} + +// AsyncCall describes a pending or completed asynchronous function call. +type AsyncCall interface { + // CallID returns the unique identifier for this async call invocation. + CallID() int64 + // Function returns the name of the function being called. + Function() string + // Overload returns the specific overload ID being invoked. + Overload() string +} + +// evalAsyncFunc is the planned Interpretable for an asynchronous function call. +type evalAsyncFunc struct { + id int64 + function string + overload string + args []InterpretableV2 + impl functions.AsyncOp +} + +// ID implements the Interpretable interface method. +func (fn *evalAsyncFunc) ID() int64 { + return fn.id +} + +// Function returns the name of the function being invoked. +func (fn *evalAsyncFunc) Function() string { + return fn.function +} + +// OverloadID returns the overload id of the function being invoked. +func (fn *evalAsyncFunc) OverloadID() string { + return fn.overload +} + +// Args returns the argument Interpretables for the function call. +func (fn *evalAsyncFunc) Args() []InterpretableV2 { + return fn.args +} + +// Eval implements the Interpretable interface method. +func (fn *evalAsyncFunc) Eval(vars Activation) ref.Val { + return EvalActivation(vars, fn.Exec) +} + +// Exec implements the InterpretableV2 interface method. +func (fn *evalAsyncFunc) Exec(frame *ExecutionFrame) ref.Val { + argVals := make([]ref.Val, len(fn.args)) + var unk *types.Unknown + for i, arg := range fn.args { + argVals[i] = arg.Exec(frame) + if types.IsError(argVals[i]) { + return argVals[i] + } + unk, _ = types.MaybeMergeUnknowns(argVals[i], unk) + } + if unk != nil { + if costs := frame.CostTracker(); costs != nil { + costs.EvalVarArgs(frame, fn.id, fn, argVals, unk) + } + return unk + } + result := frame.ComputeResult(fn.ID(), fn.Function(), fn.OverloadID(), fn.impl, argVals) + val := types.LabelErrNode(fn.id, result) + if costs := frame.CostTracker(); costs != nil { + costs.EvalVarArgs(frame, fn.id, fn, argVals, val) + } + return val +} + +// asyncCallStateTracker manages async call states across re-evaluations of a single program. +type asyncCallStateTracker struct { + mu sync.RWMutex + // calls buckets call states by a composite hash of (node id, overload, string/int/double/uint/bool args). + // A single AST node id may host many concurrently-live calls when it is evaluated inside a + // comprehension (once per element with different arguments), so each bucket may hold more + // than one state. The exact match within a bucket is resolved via asyncCallState.matches, + // which applies CEL's full equality semantics to the arguments. + calls map[uint64][]*asyncCallState + callsByID map[int64]*asyncCallState + nextCallID atomic.Int64 +} + +func newAsyncCallStateTracker() *asyncCallStateTracker { + return &asyncCallStateTracker{ + calls: make(map[uint64][]*asyncCallState), + callsByID: make(map[int64]*asyncCallState), + } +} + +const ( + hashMarkerString = 's' + hashMarkerBool = 'b' + hashMarkerNumber = 'n' + hashMarkerDefault = 'x' +) + +func hashString(s string) uint64 { + var h uint64 + for i := 0; i < len(s); i++ { + h = 31*h + uint64(s[i]) + } + return h +} + +// numericKey returns the canonical hash contribution for a CEL numeric value. +// +// Int, Uint, and Double all funnel through float64 because CEL numeric equality is +// heterogeneous and exact: any Int/Uint/Double values that compare equal convert to +// identical float64 bits. Values above 2^53 lose precision and may share a bucket, +// which is benign — findInBucket resolves it. +func numericKey(d float64) uint64 { + if math.IsNaN(d) { + // matches treats NaN as equal to NaN for dispatch purposes, so collapse all + // payloads onto one quiet-NaN pattern. + return 0x7ff8000000000000 + } + if d == 0 { + // Normalizes -0.0, which CEL equality treats as 0.0. + return 0 + } + return math.Float64bits(d) +} + +func hashArg(arg ref.Val) uint64 { + switch v := arg.(type) { + case types.String: + return hashMarkerString*31 + hashString(string(v)) + case types.Bool: + if bool(v) { + return hashMarkerBool*31 + 1 + } + return hashMarkerBool * 31 + case types.Int: + return hashMarkerNumber*31 + numericKey(float64(v)) + case types.Uint: + return hashMarkerNumber*31 + numericKey(float64(v)) + case types.Double: + return hashMarkerNumber*31 + numericKey(float64(v)) + default: + return hashMarkerDefault + } +} + +// hashCall computes the composite bucket key for an async call. +// +// Only string, int, double, uint, and bool argument values contribute to the hash. More complex types +// rely on a richer notion of equivalence (e.g. unordered maps, proto equality, custom types) +// that a byte-level hash cannot capture safely, so they are intentionally excluded from the key +// and are instead disambiguated within the bucket by asyncCallState.matches. +func hashCall(id int64, overload string, args []ref.Val) uint64 { + result := 31*uint64(id) + hashString(overload) + for _, arg := range args { + result = 31*result + hashArg(arg) + } + return result +} + +// findInBucket returns the call state in the bucket matching the same node id and call identity, +// or nil if no match is present. +func findInBucket(bucket []*asyncCallState, id int64, function, overload string, args []ref.Val) *asyncCallState { + for _, acs := range bucket { + if acs.matches(id, function, overload, args) { + return acs + } + } + return nil +} + +// getOrCreate returns the existing call state for the (node id, args) tuple, or registers and +// returns a new one. A newly registered call is assigned a unique callID and counted as pending. +func (t *asyncCallStateTracker) getOrCreate(id int64, function, overload string, argVals []ref.Val, impl functions.AsyncOp, gate *asyncGate) *asyncCallState { + key := hashCall(id, overload, argVals) + + t.mu.RLock() + acs := findInBucket(t.calls[key], id, function, overload, argVals) + t.mu.RUnlock() + if acs != nil { + return acs + } + + t.mu.Lock() + defer t.mu.Unlock() + // Check again in case it was created while waiting for the lock. + if acs := findInBucket(t.calls[key], id, function, overload, argVals); acs != nil { + return acs + } + + // Assign a new unique call ID for this async call. + acs = newAsyncCallState(id, function, overload, argVals, impl) + callID := t.nextCallID.Add(1) + acs.callID = callID + acs.gate = gate + t.calls[key] = append(t.calls[key], acs) + t.callsByID[callID] = acs + return acs +} + +func (t *asyncCallStateTracker) getByID(callID int64) *asyncCallState { + t.mu.RLock() + defer t.mu.RUnlock() + return t.callsByID[callID] +} + +func newAsyncCallState(id int64, function, overload string, argVals []ref.Val, impl functions.AsyncOp) *asyncCallState { + return &asyncCallState{ + id: id, + function: function, + overload: overload, + argVals: argVals, + impl: impl, + } +} + +// asyncCallState tracks the result of a single async function call across multiple re-evaluations. +type asyncCallState struct { + id int64 // AST expression node ID where the call is defined. + callID int64 // Unique incremental tracking ID assigned to this call. + function string + overload string + argVals []ref.Val + impl functions.AsyncOp + + mu sync.RWMutex + started bool + result ref.Val + + gate *asyncGate +} + +// CallID returns the unique identifier for this async call invocation. +func (acs *asyncCallState) CallID() int64 { + return acs.callID +} + +// Function returns the name of the function being called. +func (acs *asyncCallState) Function() string { + return acs.function +} + +// Overload returns the specific overload ID being invoked. +func (acs *asyncCallState) Overload() string { + return acs.overload +} + +// ResultOrUnknown returns the cached result if the call has completed, an Unknown +// with the call ID if pending, or nil if the call has not been started. +func (acs *asyncCallState) ResultOrUnknown() ref.Val { + if acs == nil { + return nil + } + acs.mu.RLock() + defer acs.mu.RUnlock() + if acs.result == nil && acs.started { + return types.NewUnknown(acs.callID, nil) + } + return acs.result +} + +// SetResult sets the completed result for an asynchronous function call. +func (acs *asyncCallState) SetResult(res ref.Val) { + if acs == nil { + return + } + acs.mu.Lock() + defer acs.mu.Unlock() + acs.result = res +} + +// launch returns a call's cached result, or starts the call (subject to the launch limiter) and +// returns an Unknown referencing its callID while the result is pending. +// +// Admission control: when a concurrency semaphore is configured, a launch slot is reserved with a +// non-blocking send. If no slot is free the call is left unstarted and an Unknown is returned; the +// call is retried on a later re-evaluation pass once an in-flight call completes and frees a slot. +// The reservation is non-blocking on purpose — the evaluator runs on a single goroutine, and +// blocking it here while completing calls block on an undrained completion channel would deadlock. +// The slot is held by the launched goroutine and released when it exits, so the number of live +// async goroutines is bounded by the semaphore capacity. +func (t *asyncCallStateTracker) launch(ctx context.Context, acs *asyncCallState, observer AsyncObserver) ref.Val { + if res := acs.ResultOrUnknown(); res != nil { + return res + } + gate := acs.gate + if !gate.TryAcquire() { + return types.NewUnknown(acs.callID, nil) + } + acs.mu.Lock() + if acs.started || acs.result != nil { + // Defensive: the evaluator is single-threaded so this should not happen, but if it does, + // return the reserved slot rather than leak it. + acs.mu.Unlock() + gate.Release() + return types.NewUnknown(acs.callID, nil) + } + acs.started = true + acs.mu.Unlock() + + if observer != nil { + observer.OnCallStarted(acs.callID, acs.function, acs.overload, acs.argVals) + } + go func() { + defer func() { + if observer != nil { + observer.OnCallFinished(acs.callID, acs.function, acs.overload, acs.ResultOrUnknown()) + } + gate.Complete(ctx, acs.callID) + }() + + ch := acs.impl(ctx, acs.argVals...) + // Early terminate with a CEL error when an implementation returns an empty channel. + if ch == nil { + acs.SetResult(types.NewErrFromString( + fmt.Sprintf("function %s returned an empty channel", acs.function))) + return + } + // Wait for the async computation to finish or for the context to be cancelled. + select { + case r, ok := <-ch: + if !ok { + acs.SetResult(types.NewErrFromString( + fmt.Sprintf("function %s returned an empty channel", acs.function))) + return + } + acs.SetResult(r) + case <-ctx.Done(): + // Evaluation context cancelled before the async operation completed. + acs.SetResult(types.WrapErr(context.Cause(ctx))) + } + }() + return types.NewUnknown(acs.callID, nil) +} + +// matches reports whether two call states refer to the same function, overload, and arguments. +func (acs *asyncCallState) matches(id int64, function, overload string, args []ref.Val) bool { + if acs == nil { + return false + } + if acs.id != id || acs.function != function || acs.overload != overload { + return false + } + if len(acs.argVals) != len(args) { + return false + } + for i, v := range acs.argVals { + otherV := args[i] + if types.Equal(v, otherV) == types.True { + continue + } + if n, ok := v.(types.Double); ok { + // Treat NaN as equivalent for the sake of function dispatch equality. + if otherN, ok := otherV.(types.Double); ok && math.IsNaN(float64(n)) && math.IsNaN(float64(otherN)) { + continue + } + } + return false + } + return true +} + +// trackerShrinkThreshold is the entry count above which a released tracker's maps are reallocated +// rather than cleared in place, so the pool does not retain a large backing array indefinitely. +const trackerShrinkThreshold = 1024 + +// asyncCallStateTrackerPool provides a synchronized pool of asyncCallStateTrackers. +type asyncCallTrackerPool struct { + sync.Pool +} + +func (pool *asyncCallTrackerPool) create() *asyncCallStateTracker { + return pool.Get().(*asyncCallStateTracker) +} + +func (pool *asyncCallTrackerPool) release(tracker *asyncCallStateTracker) { + if tracker == nil { + return + } + tracker.mu.Lock() + // Clearing with delete reuses the backing arrays, which is ideal for the common case but pins + // a large allocation in the pool after a wide fan-out (e.g. an async call over a big list). + // Past a threshold, reallocate so the high-water-mark memory is released to the GC instead of + // being retained by the pooled tracker. + if len(tracker.calls) > trackerShrinkThreshold || len(tracker.callsByID) > trackerShrinkThreshold { + tracker.calls = make(map[uint64][]*asyncCallState) + tracker.callsByID = make(map[int64]*asyncCallState) + } else { + for k := range tracker.calls { + delete(tracker.calls, k) + } + for k := range tracker.callsByID { + delete(tracker.callsByID, k) + } + } + tracker.nextCallID.Store(0) + tracker.mu.Unlock() + pool.Pool.Put(tracker) +} + +func newAsyncCallTrackerPool() *asyncCallTrackerPool { + return &asyncCallTrackerPool{ + Pool: sync.Pool{ + New: func() any { + return newAsyncCallStateTracker() + }, + }, + } +} + +var asyncCallStateTrackerPool = newAsyncCallTrackerPool() + +// asyncGate coordinates async call admission control and completion signaling. +type asyncGate struct { + semaphore chan struct{} + completions chan<- int64 + activeCalls atomic.Int32 +} + +func newAsyncGate(maxConcurrency int, completions chan<- int64) *asyncGate { + var sem chan struct{} + if maxConcurrency > 0 { + sem = make(chan struct{}, maxConcurrency) + } + return &asyncGate{ + semaphore: sem, + completions: completions, + } +} + +// TryAcquire attempts to acquire a concurrency slot and increments the active calls count. +func (g *asyncGate) TryAcquire() bool { + if g == nil { + return true + } + if g.semaphore != nil { + select { + case g.semaphore <- struct{}{}: + default: + return false + } + } + g.activeCalls.Add(1) + return true +} + +// Release releases a concurrency slot and decrements the active calls count (used for defensive recovery). +func (g *asyncGate) Release() { + if g == nil { + return + } + if g.semaphore != nil { + select { + case <-g.semaphore: + default: + } + } + g.activeCalls.Add(-1) +} + +// Complete releases a concurrency slot and notifies completions. +func (g *asyncGate) Complete(ctx context.Context, callID int64) { + if g == nil { + return + } + g.Release() + + if g.completions != nil { + // Prioritize context cancellation to prevent racy completion signals. + if ctx.Err() != nil { + return + } + select { + case g.completions <- callID: + case <-ctx.Done(): + } + } +} + +// ActiveCalls returns the number of active asynchronous calls. +func (g *asyncGate) ActiveCalls() int { + if g == nil { + return 0 + } + return int(g.activeCalls.Load()) +} diff --git a/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go b/vendor/cel.dev/cel-go/interpreter/attribute_patterns.go similarity index 98% rename from vendor/github.com/google/cel-go/interpreter/attribute_patterns.go rename to vendor/cel.dev/cel-go/interpreter/attribute_patterns.go index 41ca5cd219e..8e33ddd2361 100644 --- a/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go +++ b/vendor/cel.dev/cel-go/interpreter/attribute_patterns.go @@ -18,9 +18,9 @@ import ( "fmt" "strings" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // AttributePattern represents a top-level variable with an optional set of qualifier patterns. @@ -255,6 +255,9 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( patterns := vars.UnknownAttributePatterns() candidateIndices := map[int]struct{}{} for _, variable := range variableNames { + if holder, ok := vars.(localVariableHolder); ok && holder.IsLocalVariable(variable) { + continue + } for i, pat := range patterns { if pat.VariableMatches(variable) { if len(qualifiers) == 0 { diff --git a/vendor/github.com/google/cel-go/interpreter/attributes.go b/vendor/cel.dev/cel-go/interpreter/attributes.go similarity index 88% rename from vendor/github.com/google/cel-go/interpreter/attributes.go rename to vendor/cel.dev/cel-go/interpreter/attributes.go index 26d8eb0f3e4..b7c20ba346f 100644 --- a/vendor/github.com/google/cel-go/interpreter/attributes.go +++ b/vendor/cel.dev/cel-go/interpreter/attributes.go @@ -18,10 +18,10 @@ import ( "fmt" "strings" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // AttributeFactory provides methods creating Attribute and Qualifier values. @@ -173,6 +173,18 @@ func (r *attrFactory) AbsoluteAttribute(id int64, names ...string) NamespacedAtt names[idx] = strings.TrimPrefix(name, ".") } } + if len(names) == 1 && !disambiguateNames { + return &identAttribute{ + id: id, + name: names[0], + candidateNames: names, + qualifiers: []Qualifier{}, + adapter: r.adapter, + provider: r.provider, + fac: r, + errorOnBadPresenceTest: r.errorOnBadPresenceTest, + } + } return &absoluteAttribute{ id: id, namespaceNames: names, @@ -238,17 +250,29 @@ func (r *attrFactory) NewQualifier(objType *types.Type, qualID int64, val any, o // Before creating a new qualifier check to see if this is a protobuf message field access. // If so, use the precomputed GetFrom qualification method rather than the standard // stringQualifier. - str, isStr := val.(string) - if isStr && objType != nil && objType.Kind() == types.StructKind { - ft, found := r.provider.FindStructFieldType(objType.TypeName(), str) - if found && ft.IsSet != nil && ft.GetFrom != nil { - return &fieldQualifier{ - id: qualID, - Name: str, - FieldType: ft, - adapter: r.adapter, - optional: opt, - }, nil + var str string + var isStr bool + switch v := val.(type) { + case string: + str, isStr = v, true + case types.String: + str, isStr = string(v), true + } + if isStr && objType != nil { + if objType.Kind() == types.OpaqueKind && objType.TypeName() == "optional_type" && len(objType.Parameters()) == 1 { + objType = objType.Parameters()[0] + } + if objType.Kind() == types.StructKind { + ft, found := r.provider.FindStructFieldType(objType.TypeName(), str) + if found && ft.IsSet != nil && ft.GetFrom != nil { + return &fieldQualifier{ + id: qualID, + Name: str, + FieldType: ft, + adapter: r.adapter, + optional: opt, + }, nil + } } } return newQualifier(r.adapter, qualID, val, opt, r.errorOnBadPresenceTest) @@ -335,7 +359,11 @@ func (a *absoluteAttribute) Resolve(vars Activation) (any, error) { inputVars = vars wrapped, ok := inputVars.(activationWrapper) for ok { - inputVars = wrapped.Unwrap() + next := wrapped.Unwrap() + if next == nil { + break + } + inputVars = next wrapped, ok = inputVars.(activationWrapper) } } @@ -351,6 +379,13 @@ func (a *absoluteAttribute) Resolve(vars Activation) (any, error) { if celErr, ok := obj.(*types.Err); ok { return nil, celErr } + _, isUnknown := obj.(*types.Unknown) + if isUnknown { + return obj, nil + } + if len(a.qualifiers) == 0 { + return obj, nil + } obj, isOpt, err := applyQualifiers(v, obj, a.qualifiers) if err != nil { return nil, err @@ -382,6 +417,102 @@ func (a *absoluteAttribute) Resolve(vars Activation) (any, error) { return nil, missingAttribute(attrNames.String()) } +// identAttribute is a specialized, lightweight attribute for simple, single-name identifiers +// without namespace disambiguation. +type identAttribute struct { + id int64 + name string + candidateNames []string + qualifiers []Qualifier + adapter types.Adapter + provider types.Provider + fac AttributeFactory + errorOnBadPresenceTest bool +} + +// ID implements the Attribute interface method. +func (a *identAttribute) ID() int64 { + qualCount := len(a.qualifiers) + if qualCount == 0 { + return a.id + } + return a.qualifiers[qualCount-1].ID() +} + +// IsOptional returns trivially false for an attribute as the attribute represents a fully +// qualified variable name. If the attribute is used in an optional manner, then an attrQualifier +// is created and marks the attribute as optional. +func (a *identAttribute) IsOptional() bool { + return false +} + +// AddQualifier implements the Attribute interface method. +func (a *identAttribute) AddQualifier(qual Qualifier) (Attribute, error) { + a.qualifiers = append(a.qualifiers, qual) + return a, nil +} + +// CandidateVariableNames implements the NamespacedAttribute interface method. +func (a *identAttribute) CandidateVariableNames() []string { + return a.candidateNames +} + +// Qualifiers returns the list of Qualifier instances associated with the namespaced attribute. +func (a *identAttribute) Qualifiers() []Qualifier { + return a.qualifiers +} + +// Qualify is an implementation of the Qualifier interface method. +func (a *identAttribute) Qualify(vars Activation, obj any) (any, error) { + return attrQualify(a.fac, vars, obj, a) +} + +// QualifyIfPresent is an implementation of the Qualifier interface method. +func (a *identAttribute) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) { + return attrQualifyIfPresent(a.fac, vars, obj, a, presenceOnly) +} + +// String implements the fmt.Stringer interface method. +func (a *identAttribute) String() string { + return fmt.Sprintf("id: %v, names: %v", a.id, a.candidateNames) +} + +// Resolve returns the resolved Attribute value given the Activation, or error if the Attribute +// variable is not found, or if its Qualifiers cannot be applied successfully. +func (a *identAttribute) Resolve(vars Activation) (any, error) { + obj, found := vars.ResolveName(a.name) + if found { + if celErr, ok := obj.(*types.Err); ok { + return nil, celErr + } + if _, isUnknown := obj.(*types.Unknown); isUnknown { + return obj, nil + } + if len(a.qualifiers) == 0 { + return obj, nil + } + obj, isOpt, err := applyQualifiers(vars, obj, a.qualifiers) + if err != nil { + return nil, err + } + if isOpt { + val := a.adapter.NativeToValue(obj) + if types.IsUnknown(val) { + return val, nil + } + return types.OptionalOf(val), nil + } + return obj, nil + } + typ, found := a.provider.FindIdent(a.name) + if found { + if len(a.qualifiers) == 0 { + return typ, nil + } + } + return nil, missingAttribute(a.name) +} + type conditionalAttribute struct { id int64 expr InterpretableV2 @@ -613,12 +744,25 @@ func (a *relativeAttribute) QualifyIfPresent(vars Activation, obj any, presenceO // Resolve expression value and qualifier relative to the expression result. func (a *relativeAttribute) Resolve(vars Activation) (any, error) { + frame, ok := vars.(*ExecutionFrame) + if !ok { + var err error + frame, err = NewExecutionFrame(vars) + if err != nil { + return nil, err + } + defer frame.Close() + vars = frame + } // First, evaluate the operand. - v := a.operand.Eval(vars) - if types.IsError(v) { + v := a.operand.Exec(frame) + if isError(v) { return nil, v.(*types.Err) } - if types.IsUnknown(v) { + if isUnknown(v) { + return v, nil + } + if len(a.qualifiers) == 0 { return v, nil } obj, isOpt, err := applyQualifiers(vars, v, a.qualifiers) @@ -627,7 +771,7 @@ func (a *relativeAttribute) Resolve(vars Activation) (any, error) { } if isOpt { val := a.adapter.NativeToValue(obj) - if types.IsUnknown(val) { + if isUnknown(val) { return val, nil } return types.OptionalOf(val), nil @@ -905,6 +1049,14 @@ func (q *stringQualifier) qualifyInternal(vars Activation, obj any, presenceTest return obj, true, nil } default: + if nm, ok := obj.(interface{ FindStringKey(string) (any, bool) }); ok { + if v, found := nm.FindStringKey(s); found { + if presenceOnly { + return nil, true, nil + } + return v, true, nil + } + } return refQualify(q.adapter, obj, q.celValue, presenceTest, presenceOnly, q.errorOnBadPresenceTest) } if presenceTest { @@ -981,6 +1133,11 @@ func (q *intQualifier) qualifyInternal(vars Activation, obj any, presenceTest, p if isIndex { return o[i], true, nil } + case []ref.Val: + isIndex := i >= 0 && i < int64(len(o)) + if isIndex { + return o[i], true, nil + } case []string: isIndex := i >= 0 && i < int64(len(o)) if isIndex { @@ -1032,6 +1189,26 @@ func (q *intQualifier) qualifyInternal(vars Activation, obj any, presenceTest, p return o[i], true, nil } default: + if nl, ok := obj.(interface{ GetInt64Index(int64) (any, bool) }); ok { + if v, found := nl.GetInt64Index(i); found { + if presenceOnly { + return nil, true, nil + } + return v, true, nil + } + if presenceTest { + return nil, false, nil + } + return nil, false, missingIndex(q.celValue) + } + if nm, ok := obj.(interface{ FindInt64Key(int64) (any, bool) }); ok { + if v, found := nm.FindInt64Key(i); found { + if presenceOnly { + return nil, true, nil + } + return v, true, nil + } + } return refQualify(q.adapter, obj, q.celValue, presenceTest, presenceOnly, q.errorOnBadPresenceTest) } if presenceTest { @@ -1300,6 +1477,12 @@ func (q *unknownQualifier) Value() ref.Val { } func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, bool, error) { + // Early return if there's nothing to apply + if len(qualifiers) == 0 { + return obj, false, nil + } + + // Test for an optional select that's on an empty value. optObj, isOpt := obj.(*types.Optional) if isOpt { if !optObj.HasValue() { @@ -1308,6 +1491,7 @@ func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, boo obj = optObj.GetValue() } + frame, hasFrame := vars.(*ExecutionFrame) var err error for _, qual := range qualifiers { var qualObj any @@ -1315,6 +1499,12 @@ func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, boo if isOpt { var present bool qualObj, present, err = qual.QualifyIfPresent(vars, obj, false) + if hasFrame { + if costs := frame.CostTracker(); costs != nil && + (present || err != nil || costs.ModelVersion() >= 1) { + costs.Qualify(qual.ID()) + } + } if err != nil { return nil, false, err } @@ -1326,6 +1516,11 @@ func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, boo } } else { qualObj, err = qual.Qualify(vars, obj) + if hasFrame { + if costs := frame.CostTracker(); costs != nil { + costs.Qualify(qual.ID()) + } + } if err != nil { return nil, false, err } @@ -1376,7 +1571,7 @@ func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, prese val, found := v.Find(idx) // If the index is of the wrong type for the map, then it is possible // for the Find call to produce an error. - if types.IsError(val) { + if isError(val) { return nil, false, val.(*types.Err) } if found { @@ -1389,7 +1584,7 @@ func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, prese case traits.Lister: // If the index argument is not a valid numeric type, then it is possible // for the index operation to produce an error. - i, err := types.IndexOrError(idx) + i, err := types.GetOrError(idx) if err != nil { return nil, false, err } @@ -1406,7 +1601,7 @@ func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, prese ft, ok := v.(traits.FieldTester) if ok { presence := ft.IsSet(idx) - if types.IsError(presence) { + if isError(presence) { return nil, false, presence.(*types.Err) } // If not found or presence only test, then return. @@ -1417,7 +1612,7 @@ func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, prese } } val := v.Get(idx) - if types.IsError(val) { + if isError(val) { return nil, false, val.(*types.Err) } return val, true, nil diff --git a/vendor/github.com/google/cel-go/interpreter/decorators.go b/vendor/cel.dev/cel-go/interpreter/decorators.go similarity index 76% rename from vendor/github.com/google/cel-go/interpreter/decorators.go rename to vendor/cel.dev/cel-go/interpreter/decorators.go index 9c973664ac0..2b193dbb8a3 100644 --- a/vendor/github.com/google/cel-go/interpreter/decorators.go +++ b/vendor/cel.dev/cel-go/interpreter/decorators.go @@ -15,10 +15,12 @@ package interpreter import ( - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "fmt" + + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // InterpretableDecorator is a functional interface for decorating or replacing @@ -169,6 +171,77 @@ func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDe } } +func decRegexProgramSizeLimit(limit int) InterpretableDecoratorV2 { + return func(i InterpretableV2) (InterpretableV2, error) { + if limit <= 0 { + return i, nil + } + call, ok := i.(InterpretableCall) + if !ok { + return i, nil + } + if !isRegexFunction(call.Function(), call.OverloadID()) || len(call.Args()) < 2 { + return i, nil + } + regexArg := call.Args()[1] + if constVal, isConst := regexArg.(InterpretableConst); isConst { + if pattern, ok := constVal.Value().(types.String); ok { + sz, err := types.RegexProgramSize(string(pattern)) + if err != nil { + return i, nil + } + if sz > limit { + return nil, fmt.Errorf("regex program size %d exceeds limit of %d", sz, limit) + } + } + return i, nil + } + return ®exLimitCall{InterpretableCall: call, limit: limit}, nil + } +} + +func isRegexFunction(fn, overload string) bool { + switch fn { + case overloads.Matches, "regex.extract", "regex.extractAll", "regex.replace": + return true + } + switch overload { + case overloads.Matches, overloads.MatchesString, + "regex_extract_string_string", "regex_extractAll_string_string", + "regex_replace_string_string_string", "regex_replace_string_string_string_int": + return true + } + return false +} + +type regexLimitCall struct { + InterpretableCall + limit int +} + +func (r *regexLimitCall) Exec(frame *ExecutionFrame) ref.Val { + args := r.Args() + if len(args) >= 2 { + patternVal := args[1].Exec(frame) + if types.IsError(patternVal) { + return patternVal + } + if types.IsUnknown(patternVal) { + return patternVal + } + if pat, ok := patternVal.(types.String); ok { + sz, err := types.RegexProgramSize(string(pat)) + if err != nil { + return types.WrapErr(err) + } + if sz > r.limit { + return types.WrapErr(fmt.Errorf("regex program size %d exceeds limit of %d", sz, r.limit)) + } + } + } + return r.InterpretableCall.Exec(frame) +} + func maybeOptimizeConstUnary(i InterpretableV2, call InterpretableCall) (InterpretableV2, error) { args := call.Args() if len(args) != 1 { @@ -178,7 +251,9 @@ func maybeOptimizeConstUnary(i InterpretableV2, call InterpretableCall) (Interpr if !isConst { return i, nil } - val := call.Eval(EmptyActivation()) + frame, _ := NewExecutionFrame(EmptyActivation()) + val := call.Exec(frame) + frame.Close() if types.IsError(val) { return nil, val.(*types.Err) } @@ -192,7 +267,10 @@ func maybeBuildListLiteral(i InterpretableV2, l *evalList) (InterpretableV2, err return i, nil } } - return NewConstValue(l.ID(), l.Eval(EmptyActivation())), nil + frame, _ := NewExecutionFrame(EmptyActivation()) + constVal := l.Exec(frame) + frame.Close() + return NewConstValue(l.ID(), constVal), nil } func maybeBuildMapLiteral(i InterpretableV2, mp *evalMap) (InterpretableV2, error) { @@ -206,7 +284,10 @@ func maybeBuildMapLiteral(i InterpretableV2, mp *evalMap) (InterpretableV2, erro return i, nil } } - return NewConstValue(mp.ID(), mp.Eval(EmptyActivation())), nil + frame, _ := NewExecutionFrame(EmptyActivation()) + constVal := mp.Exec(frame) + frame.Close() + return NewConstValue(mp.ID(), constVal), nil } // maybeOptimizeSetMembership may convert an 'in' operation against a list to map key membership diff --git a/vendor/github.com/google/cel-go/interpreter/dispatcher.go b/vendor/cel.dev/cel-go/interpreter/dispatcher.go similarity index 98% rename from vendor/github.com/google/cel-go/interpreter/dispatcher.go rename to vendor/cel.dev/cel-go/interpreter/dispatcher.go index 8f0bdb7b8ee..40efe3009ba 100644 --- a/vendor/github.com/google/cel-go/interpreter/dispatcher.go +++ b/vendor/cel.dev/cel-go/interpreter/dispatcher.go @@ -17,7 +17,7 @@ package interpreter import ( "fmt" - "github.com/google/cel-go/common/functions" + "cel.dev/cel-go/common/functions" ) // Dispatcher resolves function calls to their appropriate overload. diff --git a/vendor/github.com/google/cel-go/interpreter/evalstate.go b/vendor/cel.dev/cel-go/interpreter/evalstate.go similarity index 97% rename from vendor/github.com/google/cel-go/interpreter/evalstate.go rename to vendor/cel.dev/cel-go/interpreter/evalstate.go index 4bdd1fdc732..c1ee6ea1c51 100644 --- a/vendor/github.com/google/cel-go/interpreter/evalstate.go +++ b/vendor/cel.dev/cel-go/interpreter/evalstate.go @@ -15,7 +15,7 @@ package interpreter import ( - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/ref" ) // EvalState tracks the values associated with expression ids during execution. diff --git a/vendor/cel.dev/cel-go/interpreter/frame.go b/vendor/cel.dev/cel-go/interpreter/frame.go new file mode 100644 index 00000000000..a6af17d0ace --- /dev/null +++ b/vendor/cel.dev/cel-go/interpreter/frame.go @@ -0,0 +1,486 @@ +// 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" + + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/types" + "cel.dev/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 + + // memory provides the context for tracking peak memory during evaluation. + memory *types.MemoryTracker + + // 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 + + // asyncCalls tracks the state of async call invocations across re-evaluations. + asyncCalls *asyncCallStateTracker + + // gate coordinates async call admission control and completion signaling. + gate *asyncGate + + // observer for monitoring async calls. + observer AsyncObserver +} + +// 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 { + // parent provides the context for parent scopes (used for comprehension iterators and nested blocks). + parent *ExecutionFrame + + // scope provides the local activation for this frame (e.g. comprehension folder, block slots, or input Activation). + scope Activation + + // functions supplies the late-bound function implementations for the evaluation, resolved + // once from the input activation. Scopes introduced during evaluation never supply function + // bindings, so a child frame inherits this value from its parent rather than resolving again. + functions FunctionActivation + + // vars holds map-based input variables directly on the frame to eliminate pool allocations. + vars map[string]any + + // lazyVars caches evaluations of lazy variables (func() any / func() ref.Val) in vars. + lazyVars map[string]any + + // ctx provides the shared evaluation state across frames. + ctx *evalContext + + // costTracker provides direct access to the active CostTracker for this evaluation pass. + costTracker *CostTracker +} + +// Scope returns the local activation scope for this frame, if present. +func (f *ExecutionFrame) Scope() Activation { + return f.scope +} + +// SetScope sets the local activation scope for this frame. +func (f *ExecutionFrame) SetScope(scope Activation) { + f.scope = scope +} + +// SetDefaultVars sets the default variables activation for the frame, composing it +// with any existing scope activation. +func (f *ExecutionFrame) SetDefaultVars(defaultVars Activation) error { + if defaultVars == nil { + return nil + } + fns, err := FindFunctionActivation(defaultVars) + if err != nil { + return err + } + if fns != nil { + if f.functions != nil { + return errNestedFunctionActivation + } + f.functions = fns + } + if f.scope != nil { + f.scope = NewHierarchicalActivation(defaultVars, f.scope) + } else { + f.scope = defaultVars + } + return nil +} + +// NewExecutionFrame creates a new execution frame from the pool. +func NewExecutionFrame(input any) (*ExecutionFrame, error) { + f := frameStack.Get().(*ExecutionFrame) + switch v := input.(type) { + case emptyActivation: + // empty frame, no backing scope needed + case Activation: + f.scope = v + fns, err := FindFunctionActivation(f.scope) + if err != nil { + frameStack.Put(f) + return nil, err + } + f.functions = fns + case map[string]any: + f.vars = v + default: + frameStack.Put(f) + 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.asyncCalls = asyncCallStateTrackerPool.create() + f.ctx.gate = &asyncGate{} + 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.gate = nil + asyncCallStateTrackerPool.release(f.ctx.asyncCalls) + f.ctx.asyncCalls = nil + f.ctx.observer = nil + f.ctx.interrupt = nil + f.ctx.state = nil + f.ctx.costs = nil + f.ctx.memory = 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 + f.costTracker = nil + f.functions = nil + if f.vars != nil { + f.vars = nil + clear(f.lazyVars) + } + f.scope = 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 +// and block 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.costTracker = f.costTracker + child.scope = activation + // Scopes pushed during evaluation never supply late-bound functions, so the child inherits + // the bindings resolved for the evaluation rather than searching its own hierarchy. + child.functions = f.functions + 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 + f.scope = nil + f.parent = nil + f.functions = nil + f.ctx = nil + f.costTracker = nil + if f.vars != nil { + f.vars = nil + clear(f.lazyVars) + } + frameStack.Put(f) + return parent +} + +// ResolveName implements the Activation interface by proxying to the internal activation. +func (f *ExecutionFrame) ResolveName(name string) (any, bool) { + if f.vars != nil { + v, found := f.vars[name] + if found { + if f.lazyVars != nil { + if resolved, found := f.lazyVars[name]; found { + return resolved, true + } + } + var lazy any + switch obj := v.(type) { + case func() ref.Val: + lazy = obj() + case func() any: + lazy = obj() + default: + return obj, true + } + if f.lazyVars == nil { + f.lazyVars = make(map[string]any, 4) + } + f.lazyVars[name] = lazy + return lazy, true + } + } + if f.scope != nil { + if val, found := f.scope.ResolveName(name); found { + return val, true + } + } + if f.parent != nil { + return f.parent.ResolveName(name) + } + return nil, false +} + +// Parent implements the Activation interface by proxying to the parent frame or scope. +func (f *ExecutionFrame) Parent() Activation { + if f.parent != nil { + if f.parent.scope != nil { + return f.parent.scope + } + return f.parent + } + if f.scope != nil { + return f.scope.Parent() + } + return nil +} + +// ResolveFunction implements the FunctionActivation interface using the bindings resolved when +// the evaluation began, so the cost of a lookup does not depend on the number of enclosing scopes. +func (f *ExecutionFrame) ResolveFunction(name string) (functions.LateBoundOp, bool) { + if f.functions == nil { + return nil, false + } + return f.functions.ResolveFunction(name) +} + +// AsPartialActivation implements the PartialActivation interface by proxying to the internal scope or parent. +func (f *ExecutionFrame) AsPartialActivation() (PartialActivation, bool) { + if f.scope != nil { + if pa, ok := AsPartialActivation(f.scope); ok { + return pa, true + } + } + if f.parent != nil { + return f.parent.AsPartialActivation() + } + return nil, false +} + +// UnknownAttributePatterns implements the PartialActivation interface returning the unknown patterns +// if they were provided to the input activation, or an empty set if the frame is not partial. +func (f *ExecutionFrame) UnknownAttributePatterns() []*AttributePattern { + if pa, ok := f.AsPartialActivation(); ok { + return pa.UnknownAttributePatterns() + } + return []*AttributePattern{} +} + +// Unwrap returns the local activation scope if present, or the parent frame. +func (f *ExecutionFrame) Unwrap() Activation { + if f.scope != nil { + return f.scope + } + if f.parent != nil { + return f.parent + } + return nil +} + +// IsLocalVariable reports whether the variable name is locally bound in the frame. +func (f *ExecutionFrame) IsLocalVariable(name string) bool { + if holder, ok := f.scope.(localVariableHolder); ok { + if holder.IsLocalVariable(name) { + return true + } + } + // Search parent scopes + if f.parent != nil { + return f.parent.IsLocalVariable(name) + } + return false +} + +// 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 +} + +// CostTracker returns the active CostTracker for this evaluation pass, or nil. +func (f *ExecutionFrame) CostTracker() *CostTracker { + if f == nil { + return nil + } + return f.costTracker +} + +// SetCostTracker sets the active CostTracker for this evaluation pass. +func (f *ExecutionFrame) SetCostTracker(tracker *CostTracker) { + if f.ctx == nil { + f.ctx = evalContextPool.Get().(*evalContext) + } + f.ctx.costs = tracker + f.costTracker = tracker +} + +// ComputeResult tracks and computes the result of the given asynchronous function. +// +// The first invocation for a given (node id, args) tuple registers the call state and returns an +// Unknown which references the call's unique callID. Subsequent invocations return the cached +// result once the call has completed. Launching background execution is deferred to post-execution +// dispatch via DispatchPendingAsyncCalls. +func (f *ExecutionFrame) ComputeResult(id int64, function, overload string, impl functions.AsyncOp, argVals []ref.Val) ref.Val { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return types.NewErrWithNodeID(id, "asynchronous function calls require concurrent evaluation and cannot be resolved by a synchronous Eval") + } + t := f.ctx.asyncCalls + acs := t.getOrCreate(id, function, overload, argVals, impl, f.ctx.gate) + if res := acs.ResultOrUnknown(); res != nil { + return res + } + return types.NewUnknown(acs.callID, nil) +} + +// DispatchPendingAsyncCalls launches pending asynchronous calls for the specified required call IDs. +func (f *ExecutionFrame) DispatchPendingAsyncCalls(callIDs []int64) { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return + } + t := f.ctx.asyncCalls + for _, callID := range callIDs { + if acs := t.getByID(callID); acs != nil { + t.launch(f.ctx.ctx, acs, f.ctx.observer) + } + } +} + +// ActiveAsyncCalls returns the number of async function calls that have been launched +// but whose completions have not yet been drained. +func (f *ExecutionFrame) ActiveAsyncCalls() int { + if f.ctx == nil || f.ctx.gate == nil { + return 0 + } + return f.ctx.gate.ActiveCalls() +} + +// AsyncCall returns the state of an async call by its callID, or nil if not found. +func (f *ExecutionFrame) AsyncCall(callID int64) AsyncCall { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return nil + } + acs := f.ctx.asyncCalls.getByID(callID) + if acs == nil { + return nil + } + return acs +} + +// SetCompletions configures a channel to receive callIDs when asynchronous evaluations finish. +func (f *ExecutionFrame) SetCompletions(ch chan<- int64) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + f.ctx.gate.completions = ch + return nil +} + +// SetAsyncObserver sets the observer for monitoring asynchronous function calls. +func (f *ExecutionFrame) SetAsyncObserver(observer AsyncObserver) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + f.ctx.observer = observer + return nil +} + +// SetAsyncMaxConcurrency sets the maximum concurrency for asynchronous function calls. +// +// A non-positive value indicates that concurrency is unbounded. +func (f *ExecutionFrame) SetAsyncMaxConcurrency(n int) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + if n > 0 { + f.ctx.gate.semaphore = make(chan struct{}, n) + } else { + f.ctx.gate.semaphore = nil + } + return nil +} + +// 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{} + }, +} diff --git a/vendor/github.com/google/cel-go/interpreter/functions/BUILD.bazel b/vendor/cel.dev/cel-go/interpreter/functions/BUILD.bazel similarity index 82% rename from vendor/github.com/google/cel-go/interpreter/functions/BUILD.bazel rename to vendor/cel.dev/cel-go/interpreter/functions/BUILD.bazel index 4a80c3ea085..e438b126b4c 100644 --- a/vendor/github.com/google/cel-go/interpreter/functions/BUILD.bazel +++ b/vendor/cel.dev/cel-go/interpreter/functions/BUILD.bazel @@ -10,7 +10,7 @@ go_library( srcs = [ "functions.go", ], - importpath = "github.com/google/cel-go/interpreter/functions", + importpath = "cel.dev/cel-go/interpreter/functions", deps = [ "//common/functions:go_default_library", ], diff --git a/vendor/github.com/google/cel-go/interpreter/functions/functions.go b/vendor/cel.dev/cel-go/interpreter/functions/functions.go similarity index 96% rename from vendor/github.com/google/cel-go/interpreter/functions/functions.go rename to vendor/cel.dev/cel-go/interpreter/functions/functions.go index 21ffb69246a..02123f1ab67 100644 --- a/vendor/github.com/google/cel-go/interpreter/functions/functions.go +++ b/vendor/cel.dev/cel-go/interpreter/functions/functions.go @@ -16,7 +16,7 @@ // interpreter and as declared within the checker#StandardDeclarations. package functions -import fn "github.com/google/cel-go/common/functions" +import fn "cel.dev/cel-go/common/functions" // Overload defines a named overload of a function, indicating an operand trait // which must be present on the first argument to the overload as well as one diff --git a/vendor/github.com/google/cel-go/interpreter/interpretable.go b/vendor/cel.dev/cel-go/interpreter/interpretable.go similarity index 66% rename from vendor/github.com/google/cel-go/interpreter/interpretable.go rename to vendor/cel.dev/cel-go/interpreter/interpretable.go index 5b94e23e6c2..2759a1f336e 100644 --- a/vendor/github.com/google/cel-go/interpreter/interpretable.go +++ b/vendor/cel.dev/cel-go/interpreter/interpretable.go @@ -18,12 +18,12 @@ import ( "fmt" "sync" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) // Interpretable evaluates an Activation and produces a value. @@ -161,7 +161,7 @@ func (oi *ObservableInterpretable) Exec(frame *ExecutionFrame) ref.Val { // Eval proxies to the ObserveEval method while invoking a no-op callback to report the observations. func (oi *ObservableInterpretable) Eval(vars Activation) ref.Val { - return oi.ObserveExec(AsFrame(vars), func(any) {}) + return oi.ObserveEval(vars, func(any) {}) } // ObserveEval evaluates an interpretable and performs per-evaluation state-tracking. @@ -169,7 +169,16 @@ func (oi *ObservableInterpretable) Eval(vars Activation) ref.Val { // This method is concurrency safe and the expectation is that the observer function will use // a switch statement to determine the type of the state which has been reported back from the call. func (oi *ObservableInterpretable) ObserveEval(vars Activation, observer func(any)) ref.Val { - return oi.ObserveExec(AsFrame(vars), observer) + frame, ok := vars.(*ExecutionFrame) + if !ok { + var err error + frame, err = NewExecutionFrame(vars) + if err != nil { + return types.NewErr("invalid activation: %v", err) + } + defer frame.Close() + } + return oi.ObserveExec(frame, observer) } // ObserveExec evaluates an interpretable and performs per-evaluation state-tracking. @@ -195,18 +204,19 @@ func (oi *ObservableInterpretable) ObserveExec(frame *ExecutionFrame, observer f return result } -// AsFrame promotes an Activation to an ExecutionFrame. -func AsFrame(a Activation) *ExecutionFrame { - if f, ok := a.(*ExecutionFrame); ok { - return f +// EvalActivation runs an InterpretableV2 Exec method safely with any Activation, +// wrapping it in a pooled ExecutionFrame if needed and releasing it after execution. +func EvalActivation(ctx Activation, exec func(*ExecutionFrame) ref.Val) ref.Val { + if f, ok := ctx.(*ExecutionFrame); ok { + return exec(f) } - frame := &ExecutionFrame{Activation: a} - // Walk the activation hierarchy to find a parent ExecutionFrame and inherit - // its shared context. - if parent := findFrame(a); parent != nil { - frame.ctx = parent.ctx + f, err := NewExecutionFrame(ctx) + if err != nil { + return types.NewErr("invalid activation: %v", err) } - return frame + res := exec(f) + f.Close() + return res } // findFrame walks the activation hierarchy via Unwrap and Parent to locate an @@ -243,19 +253,24 @@ func (test *evalTestOnly) ID() int64 { // Exec implements the InterpretableV2 interface method. func (test *evalTestOnly) Exec(frame *ExecutionFrame) ref.Val { val, err := test.Resolve(frame) + var res ref.Val // Return an error if the resolve step fails if err != nil { - return types.LabelErrNode(test.id, types.WrapErr(err)) + res = types.LabelErrNode(test.id, types.WrapErr(err)) + } else if optVal, isOpt := val.(*types.Optional); isOpt { + res = types.Bool(optVal.HasValue()) + } else { + res = test.Adapter().NativeToValue(val) } - if optVal, isOpt := val.(*types.Optional); isOpt { - return types.Bool(optVal.HasValue()) + if costs := frame.CostTracker(); costs != nil { + costs.EvalAttribute(test.id, true, res) } - return test.Adapter().NativeToValue(val) + return res } // Eval implements the Interpretable interface method. func (test *evalTestOnly) Eval(ctx Activation) ref.Val { - return test.Exec(AsFrame(ctx)) + return EvalActivation(ctx, test.Exec) } // AddQualifier appends a qualifier that will always and only perform a presence test. @@ -350,10 +365,14 @@ func (or *evalOr) Exec(frame *ExecutionFrame) ref.Val { return types.True } if !ok { - isUnk := false - unk, isUnk = types.MaybeMergeUnknowns(val, unk) - if !isUnk && err == nil { - if types.IsError(val) { + if unkVal, isUnk := val.(*types.Unknown); isUnk { + if unk == nil { + unk = unkVal + } else { + unk = types.MergeUnknowns(unk, unkVal) + } + } else if err == nil { + if isError(val) { err = val } else { err = types.MaybeNoSuchOverloadErr(val) @@ -373,7 +392,7 @@ func (or *evalOr) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (or *evalOr) Eval(ctx Activation) ref.Val { - return or.Exec(AsFrame(ctx)) + return EvalActivation(ctx, or.Exec) } type evalAnd struct { @@ -398,10 +417,14 @@ func (and *evalAnd) Exec(frame *ExecutionFrame) ref.Val { return types.False } if !ok { - isUnk := false - unk, isUnk = types.MaybeMergeUnknowns(val, unk) - if !isUnk && err == nil { - if types.IsError(val) { + if unkVal, isUnk := val.(*types.Unknown); isUnk { + if unk == nil { + unk = unkVal + } else { + unk = types.MergeUnknowns(unk, unkVal) + } + } else if err == nil { + if isError(val) { err = val } else { err = types.MaybeNoSuchOverloadErr(val) @@ -421,7 +444,7 @@ func (and *evalAnd) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (and *evalAnd) Eval(ctx Activation) ref.Val { - return and.Exec(AsFrame(ctx)) + return EvalActivation(ctx, and.Exec) } type evalEq struct { @@ -438,19 +461,40 @@ func (eq *evalEq) ID() int64 { // Exec implements the InterpretableV2 interface method. func (eq *evalEq) Exec(frame *ExecutionFrame) ref.Val { lVal := eq.lhs.Exec(frame) - rVal := eq.rhs.Exec(frame) - if types.IsUnknownOrError(lVal) { + if isError(lVal) { + // To preserve legacy cost tracking behavior for == + // track this cost, but it will be removed in the future. + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, eq.id, eq, lVal, types.UnknownType, lVal) + } return lVal } - if types.IsUnknownOrError(rVal) { + rVal := eq.rhs.Exec(frame) + if isError(rVal) { + // To preserve legacy cost tracking behavior for ==, + // track this cost, but it will be removed in the future. + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, eq.id, eq, lVal, rVal, rVal) + } return rVal } - return types.Equal(lVal, rVal) + if isUnknown(lVal) || isUnknown(rVal) { + unk := mergeBinaryUnknowns(lVal, rVal) + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, eq.id, eq, lVal, rVal, unk) + } + return unk + } + res := types.Equal(lVal, rVal) + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, eq.id, eq, lVal, rVal, res) + } + return res } // Eval implements the Interpretable interface method. func (eq *evalEq) Eval(ctx Activation) ref.Val { - return eq.Exec(AsFrame(ctx)) + return EvalActivation(ctx, eq.Exec) } // Function implements the InterpretableCall interface method. @@ -482,19 +526,40 @@ func (ne *evalNe) ID() int64 { // Exec implements the InterpretableV2 interface method. func (ne *evalNe) Exec(frame *ExecutionFrame) ref.Val { lVal := ne.lhs.Exec(frame) - rVal := ne.rhs.Exec(frame) - if types.IsUnknownOrError(lVal) { + if isError(lVal) { + // To preserve legacy cost tracking behavior for !=, + // track this cost, but it will be removed in the future. + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, ne.id, ne, lVal, types.UnknownType, lVal) + } return lVal } - if types.IsUnknownOrError(rVal) { + rVal := ne.rhs.Exec(frame) + if isError(rVal) { + // To preserve legacy cost tracking behavior for !=, + // track this cost, but it will be removed in the future. + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, ne.id, ne, lVal, rVal, rVal) + } return rVal } - return types.Bool(types.Equal(lVal, rVal) != types.True) + if isUnknown(lVal) || isUnknown(rVal) { + unk := mergeBinaryUnknowns(lVal, rVal) + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, ne.id, ne, lVal, rVal, unk) + } + return unk + } + res := types.Bool(types.Equal(lVal, rVal) != types.True) + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, ne.id, ne, lVal, rVal, res) + } + return res } // Eval implements the Interpretable interface method. func (ne *evalNe) Eval(ctx Activation) ref.Val { - return ne.Exec(AsFrame(ctx)) + return EvalActivation(ctx, ne.Exec) } // Function implements the InterpretableCall interface method. @@ -526,12 +591,16 @@ func (zero *evalZeroArity) ID() int64 { // Exec implements the InterpretableV2 interface method. func (zero *evalZeroArity) Exec(frame *ExecutionFrame) ref.Val { - return types.LabelErrNode(zero.id, zero.impl()) + res := labelErrNode(zero.id, zero.impl()) + if costs := frame.CostTracker(); costs != nil { + costs.EvalZeroArity(frame, zero.id, zero, res) + } + return res } // Eval implements the Interpretable interface method. func (zero *evalZeroArity) Eval(ctx Activation) ref.Val { - return zero.Exec(AsFrame(ctx)) + return EvalActivation(ctx, zero.Exec) } // Function implements the InterpretableCall interface method. @@ -544,7 +613,7 @@ func (zero *evalZeroArity) OverloadID() string { return zero.overload } -// Args returns the argument to the unary function. +// Args implements the InterpretableCall interface method. func (zero *evalZeroArity) Args() []InterpretableV2 { return []InterpretableV2{} } @@ -567,27 +636,47 @@ func (un *evalUnary) ID() int64 { // Exec implements the InterpretableV2 interface method. func (un *evalUnary) Exec(frame *ExecutionFrame) ref.Val { argVal := un.arg.Exec(frame) - // Early return if the argument to the function is unknown or error. + // Early return if the argument to the function is error or unknown in strict mode. strict := !un.nonStrict - if strict && types.IsUnknownOrError(argVal) { - return argVal + if strict { + switch v := argVal.(type) { + case *types.Err: + // To preserve legacy cost tracking behavior for logical not, + // this cost is tracked, but will be removed in the future. + if un.function == operators.LogicalNot { + if costs := frame.CostTracker(); costs != nil { + costs.EvalUnary(frame, un.id, un, argVal, argVal) + } + } + return v + case *types.Unknown: + if costs := frame.CostTracker(); costs != nil { + costs.EvalUnary(frame, un.id, un, argVal, argVal) + } + return v + } } + var res ref.Val // If the implementation is bound and the argument value has the right traits required to // invoke it, then call the implementation. - if un.impl != nil && (un.trait == 0 || (!strict && types.IsUnknownOrError(argVal)) || argVal.Type().HasTrait(un.trait)) { - return types.LabelErrNode(un.id, un.impl(argVal)) + if un.impl != nil && (un.trait == 0 || matchOperandTrait(un.trait, strict, argVal)) { + res = labelErrNode(un.id, un.impl(argVal)) + } else if argVal.Type().HasTrait(traits.ReceiverType) { + // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the + // operand (arg0). + res = labelErrNode(un.id, argVal.(traits.Receiver).Receive(un.function, un.overload, []ref.Val{})) + } else { + res = types.NewErrWithNodeID(un.id, "no such overload: %s", un.function) } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if argVal.Type().HasTrait(traits.ReceiverType) { - return types.LabelErrNode(un.id, argVal.(traits.Receiver).Receive(un.function, un.overload, []ref.Val{})) + if costs := frame.CostTracker(); costs != nil { + costs.EvalUnary(frame, un.id, un, argVal, res) } - return types.NewErrWithNodeID(un.id, "no such overload: %s", un.function) + return res } // Eval implements the Interpretable interface method. func (un *evalUnary) Eval(ctx Activation) ref.Val { - return un.Exec(AsFrame(ctx)) + return EvalActivation(ctx, un.Exec) } // Function implements the InterpretableCall interface method. @@ -624,33 +713,59 @@ func (bin *evalBinary) ID() int64 { // Exec implements the InterpretableV2 interface method. func (bin *evalBinary) Exec(frame *ExecutionFrame) ref.Val { lVal := bin.lhs.Exec(frame) - rVal := bin.rhs.Exec(frame) - // Early return if any argument to the function is unknown or error. strict := !bin.nonStrict if strict { - if types.IsUnknownOrError(lVal) { + switch lVal.(type) { + case *types.Err: + return lVal + case *types.Unknown: + rVal := bin.rhs.Exec(frame) + if unk, ok := rVal.(*types.Unknown); ok { + res := types.MergeUnknowns(lVal.(*types.Unknown), unk) + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, bin.id, bin, lVal, rVal, res) + } + return res + } + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, bin.id, bin, lVal, rVal, lVal) + } return lVal } - if types.IsUnknownOrError(rVal) { + } + rVal := bin.rhs.Exec(frame) + if strict { + switch rVal.(type) { + case *types.Err: + return rVal + case *types.Unknown: + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, bin.id, bin, lVal, rVal, rVal) + } return rVal } } + var res ref.Val // If the implementation is bound and the argument value has the right traits required to // invoke it, then call the implementation. - if bin.impl != nil && (bin.trait == 0 || (!strict && types.IsUnknownOrError(lVal)) || lVal.Type().HasTrait(bin.trait)) { - return types.LabelErrNode(bin.id, bin.impl(lVal, rVal)) + if bin.impl != nil && (bin.trait == 0 || matchOperandTrait(bin.trait, strict, lVal)) { + res = labelErrNode(bin.id, bin.impl(lVal, rVal)) + } else if lVal.Type().HasTrait(traits.ReceiverType) { + // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the + // operand (arg0). + res = labelErrNode(bin.id, lVal.(traits.Receiver).Receive(bin.function, bin.overload, []ref.Val{rVal})) + } else { + res = types.NewErrWithNodeID(bin.id, "no such overload: %s", bin.function) } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if lVal.Type().HasTrait(traits.ReceiverType) { - return types.LabelErrNode(bin.id, lVal.(traits.Receiver).Receive(bin.function, bin.overload, []ref.Val{rVal})) + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, bin.id, bin, lVal, rVal, res) } - return types.NewErrWithNodeID(bin.id, "no such overload: %s", bin.function) + return res } // Eval implements the Interpretable interface method. func (bin *evalBinary) Eval(ctx Activation) ref.Val { - return bin.Exec(AsFrame(ctx)) + return EvalActivation(ctx, bin.Exec) } // Function implements the InterpretableCall interface method. @@ -697,31 +812,64 @@ func (fn *evalVarArgs) ID() int64 { // Exec implements the InterpretableV2 interface method. func (fn *evalVarArgs) Exec(frame *ExecutionFrame) ref.Val { argVals := make([]ref.Val, len(fn.args)) - // Early return if any argument to the function is unknown or error. strict := !fn.nonStrict + var unk *types.Unknown for i, arg := range fn.args { - argVals[i] = arg.Exec(frame) - if strict && types.IsUnknownOrError(argVals[i]) { - return argVals[i] + argVal := arg.Exec(frame) + if strict { + switch v := argVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + } } + argVals[i] = argVal } + if strict && unk != nil { + if costs := frame.CostTracker(); costs != nil { + costs.EvalVarArgs(frame, fn.id, fn, argVals, unk) + } + return unk + } + if len(argVals) == 0 { + var res ref.Val + if fn.impl != nil { + res = labelErrNode(fn.id, fn.impl()) + } else { + res = types.NewErrWithNodeID(fn.id, "no such overload: %s %d", fn.function, fn.id) + } + if costs := frame.CostTracker(); costs != nil { + costs.EvalZeroArity(frame, fn.id, fn, res) + } + return res + } + var res ref.Val // If the implementation is bound and the argument value has the right traits required to // invoke it, then call the implementation. arg0 := argVals[0] - if fn.impl != nil && (fn.trait == 0 || (!strict && types.IsUnknownOrError(arg0)) || arg0.Type().HasTrait(fn.trait)) { - return types.LabelErrNode(fn.id, fn.impl(argVals...)) + if fn.impl != nil && matchOperandTrait(fn.trait, strict, arg0) { + res = labelErrNode(fn.id, fn.impl(argVals...)) + } else if arg0.Type().HasTrait(traits.ReceiverType) { + // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the + // operand (arg0). + res = labelErrNode(fn.id, arg0.(traits.Receiver).Receive(fn.function, fn.overload, argVals[1:])) + } else { + res = types.NewErrWithNodeID(fn.id, "no such overload: %s %d", fn.function, fn.id) } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if arg0.Type().HasTrait(traits.ReceiverType) { - return types.LabelErrNode(fn.id, arg0.(traits.Receiver).Receive(fn.function, fn.overload, argVals[1:])) + if costs := frame.CostTracker(); costs != nil { + costs.EvalVarArgs(frame, fn.id, fn, argVals, res) } - return types.NewErrWithNodeID(fn.id, "no such overload: %s %d", fn.function, fn.id) + return res } // Eval implements the Interpretable interface method. func (fn *evalVarArgs) Eval(ctx Activation) ref.Val { - return fn.Exec(AsFrame(ctx)) + return EvalActivation(ctx, fn.Exec) } // Function implements the InterpretableCall interface method. @@ -734,11 +882,140 @@ func (fn *evalVarArgs) OverloadID() string { return fn.overload } -// Args returns the argument to the unary function. +// Args returns the normalized arguments to the function overload. func (fn *evalVarArgs) Args() []InterpretableV2 { return fn.args } +// evalLateBoundFunc represents a function call where the function implementation is resolved from +// the Activation at evaluation time. +type evalLateBoundFunc struct { + id int64 + function string + overload string + args []InterpretableV2 + trait int + nonStrict bool + member bool + dispatch functions.LateBoundDispatcher +} + +// ID implements the Interpretable interface method. +func (fn *evalLateBoundFunc) ID() int64 { + return fn.id +} + +// Function implements the InterpretableCall interface method. +func (fn *evalLateBoundFunc) Function() string { + return fn.function +} + +// OverloadID implements the InterpretableCall interface method. +func (fn *evalLateBoundFunc) OverloadID() string { + return fn.overload +} + +// Args returns the argument Interpretables for the function call. +func (fn *evalLateBoundFunc) Args() []InterpretableV2 { + return fn.args +} + +// Eval implements the Interpretable interface method. +func (fn *evalLateBoundFunc) Eval(vars Activation) ref.Val { + return EvalActivation(vars, fn.Exec) +} + +// Exec implements the InterpretableV2 interface method. +func (fn *evalLateBoundFunc) Exec(frame *ExecutionFrame) ref.Val { + argVals := make([]ref.Val, len(fn.args)) + strict := !fn.nonStrict + var unk *types.Unknown + for i, arg := range fn.args { + argVals[i] = arg.Exec(frame) + if strict { + if types.IsError(argVals[i]) { + return argVals[i] + } + unk, _ = types.MaybeMergeUnknowns(argVals[i], unk) + } + } + if strict && unk != nil { + if costs := frame.CostTracker(); costs != nil { + costs.EvalVarArgs(frame, fn.id, fn, argVals, unk) + } + return unk + } + var res ref.Val + // A late-bound function is only invoked when the runtime arguments agree with a declared + // overload and the activation supplies a compatible implementation. Unlike other call + // types, there is no fallback to receiver-style dispatch on the operand: the function + // declaration promised a binding supplied at evaluation time, so a missing or mismatched + // binding is unambiguously an error. + if lateFn, found := fn.resolve(frame, argVals, strict); found { + res = types.LabelErrNode(fn.id, lateFn(argVals...)) + } else { + res = types.NewErrWithNodeID(fn.id, "no such overload: %s", fn.function) + } + switch len(argVals) { + case 0: + if costs := frame.CostTracker(); costs != nil { + costs.EvalZeroArity(frame, fn.id, fn, res) + } + case 1: + if costs := frame.CostTracker(); costs != nil { + costs.EvalUnary(frame, fn.id, fn, argVals[0], res) + } + case 2: + if costs := frame.CostTracker(); costs != nil { + costs.EvalBinary(frame, fn.id, fn, argVals[0], argVals[1], res) + } + default: + if costs := frame.CostTracker(); costs != nil { + costs.EvalVarArgs(frame, fn.id, fn, argVals, res) + } + } + return res +} + +// resolve determines which declared overload the runtime arguments satisfy and resolves its +// implementation from the activation. +func (fn *evalLateBoundFunc) resolve(frame *ExecutionFrame, args []ref.Val, strict bool) (functions.FunctionOp, bool) { + overloadID, matched := fn.matchOverload(args, strict) + if !matched { + return nil, false + } + impl, found := frame.ResolveFunction(fn.function) + if !found { + return nil, false + } + return func(args ...ref.Val) ref.Val { return impl(overloadID, args...) }, true +} + +// matchOverload reports the id of the declared overload which matches the runtime arguments. +// +// Plans which were not created from a function declaration have no signature to check against, +// in which case the call falls back to the operand trait recorded for the call site. +func (fn *evalLateBoundFunc) matchOverload(args []ref.Val, strict bool) (string, bool) { + if fn.dispatch != nil { + return fn.dispatch(fn.member, args...) + } + if len(args) == 0 || matchOperandTrait(fn.trait, strict, args[0]) { + return fn.overload, true + } + return "", false +} + +// matchOperandTrait returns true if the operand satisfies the trait required by the function +// implementation, or if the implementation does not require a trait. +// +// Non-strict functions are given the chance to handle unknown and error operands themselves. +func matchOperandTrait(trait int, strict bool, operand ref.Val) bool { + return trait == 0 || + (!strict && types.IsUnknownOrError(operand)) || + operand.Type().HasTrait(trait) +} + +// evalList evaluates a list construction expression. type evalList struct { id int64 elems []InterpretableV2 @@ -754,31 +1031,74 @@ func (l *evalList) ID() int64 { // Exec implements the InterpretableV2 interface method. func (l *evalList) Exec(frame *ExecutionFrame) ref.Val { + var unk *types.Unknown + if !l.hasOptionals { + elemVals := make([]ref.Val, len(l.elems)) + for i, elem := range l.elems { + elemVal := elem.Exec(frame) + switch v := elemVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + } + elemVals[i] = elemVal + } + if unk != nil { + trackCostCreateList(frame, l.id, unk) + return unk + } + res := types.NewRefValList(l.adapter, elemVals) + trackCostCreateList(frame, l.id, res) + return res + } + elemVals := make([]ref.Val, 0, len(l.elems)) - // If any argument is unknown or error early terminate. for i, elem := range l.elems { elemVal := elem.Exec(frame) - if types.IsUnknownOrError(elemVal) { - return elemVal - } - if l.hasOptionals && l.optionals[i] { - optVal, ok := elemVal.(*types.Optional) - if !ok { + switch v := elemVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + case *types.Optional: + if l.optionals[i] { + if !v.HasValue() { + continue + } + elemVal = v.GetValue() + } + default: + if l.optionals[i] { return types.LabelErrNode(l.id, invalidOptionalElementInit(elemVal)) } - if !optVal.HasValue() { - continue - } - elemVal = optVal.GetValue() } elemVals = append(elemVals, elemVal) } - return types.NewRefValList(l.adapter, elemVals) + if unk != nil { + if costs := frame.CostTracker(); costs != nil { + costs.CreateList(l.id, unk) + } + return unk + } + res := types.NewRefValList(l.adapter, elemVals) + if costs := frame.CostTracker(); costs != nil { + costs.CreateList(l.id, res) + } + return res } // Eval implements the Interpretable interface method. func (l *evalList) Eval(ctx Activation) ref.Val { - return l.Exec(AsFrame(ctx)) + return EvalActivation(ctx, l.Exec) } func (l *evalList) InitVals() []InterpretableV2 { @@ -806,35 +1126,97 @@ func (m *evalMap) ID() int64 { // Exec implements the InterpretableV2 interface method. func (m *evalMap) Exec(frame *ExecutionFrame) ref.Val { entries := make(map[ref.Val]ref.Val, len(m.keys)) - // If any argument is unknown or error early terminate. + var unk *types.Unknown + if !m.hasOptionals { + for i, key := range m.keys { + keyVal := key.Exec(frame) + switch v := keyVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + } + + valVal := m.vals[i].Exec(frame) + switch v := valVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + } + entries[keyVal] = valVal + } + if unk != nil { + trackCostCreateMap(frame, m.id, unk) + return unk + } + res := types.NewRefValMap(m.adapter, entries) + trackCostCreateMap(frame, m.id, res) + return res + } + for i, key := range m.keys { keyVal := key.Exec(frame) - if types.IsUnknownOrError(keyVal) { - return keyVal + switch v := keyVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } } + valVal := m.vals[i].Exec(frame) - if types.IsUnknownOrError(valVal) { - return valVal - } - if m.hasOptionals && m.optionals[i] { - optVal, ok := valVal.(*types.Optional) - if !ok { + switch v := valVal.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + case *types.Optional: + if m.optionals[i] { + if !v.HasValue() { + delete(entries, keyVal) + continue + } + valVal = v.GetValue() + } + default: + if m.optionals[i] { return types.LabelErrNode(m.id, invalidOptionalEntryInit(keyVal, valVal)) } - if !optVal.HasValue() { - delete(entries, keyVal) - continue - } - valVal = optVal.GetValue() } entries[keyVal] = valVal } - return types.NewRefValMap(m.adapter, entries) + if unk != nil { + if costs := frame.CostTracker(); costs != nil { + costs.CreateMap(m.id, unk) + } + return unk + } + res := types.NewRefValMap(m.adapter, entries) + if costs := frame.CostTracker(); costs != nil { + costs.CreateMap(m.id, res) + } + return res } // Eval implements the Interpretable interface method. func (m *evalMap) Eval(ctx Activation) ref.Val { - return m.Exec(AsFrame(ctx)) + return EvalActivation(ctx, m.Exec) } func (m *evalMap) InitVals() []InterpretableV2 { @@ -875,31 +1257,73 @@ func (o *evalObj) ID() int64 { // Exec implements the InterpretableV2 interface method. func (o *evalObj) Exec(frame *ExecutionFrame) ref.Val { fieldVals := make(map[string]ref.Val, len(o.fields)) - // If any argument is unknown or error early terminate. + var unk *types.Unknown + if !o.hasOptionals { + for i, field := range o.fields { + val := o.vals[i].Exec(frame) + switch v := val.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + } + fieldVals[field] = val + } + if unk != nil { + trackCostCreateStruct(frame, o.id, unk) + return unk + } + res := labelErrNode(o.id, o.provider.NewValue(o.typeName, fieldVals)) + trackCostCreateStruct(frame, o.id, res) + return res + } + for i, field := range o.fields { val := o.vals[i].Exec(frame) - if types.IsUnknownOrError(val) { - return val - } - if o.hasOptionals && o.optionals[i] { - optVal, ok := val.(*types.Optional) - if !ok { + switch v := val.(type) { + case *types.Err: + return v + case *types.Unknown: + if unk == nil { + unk = v + } else { + unk = types.MergeUnknowns(unk, v) + } + case *types.Optional: + if o.optionals[i] { + if !v.HasValue() { + delete(fieldVals, field) + continue + } + val = v.GetValue() + } + default: + if o.optionals[i] { return types.LabelErrNode(o.id, invalidOptionalEntryInit(field, val)) } - if !optVal.HasValue() { - delete(fieldVals, field) - continue - } - val = optVal.GetValue() } fieldVals[field] = val } - return types.LabelErrNode(o.id, o.provider.NewValue(o.typeName, fieldVals)) + if unk != nil { + if costs := frame.CostTracker(); costs != nil { + costs.CreateStruct(o.id, unk) + } + return unk + } + res := labelErrNode(o.id, o.provider.NewValue(o.typeName, fieldVals)) + if costs := frame.CostTracker(); costs != nil { + costs.CreateStruct(o.id, res) + } + return res } // Eval implements the Interpretable interface method. func (o *evalObj) Eval(ctx Activation) ref.Val { - return o.Exec(AsFrame(ctx)) + return EvalActivation(ctx, o.Exec) } // InitVals implements the InterpretableConstructor interface method. @@ -924,6 +1348,11 @@ type evalFold struct { result InterpretableV2 adapter types.Adapter + // mutableAccu indicates that static analysis of the comprehension has determined that the + // accumulator is only ever observed by in-place accumulation steps and the final result, and + // may therefore be backed by a mutable list or map without the mutation being observable. + mutableAccu bool + // note an exhaustive fold will ensure that all branches are evaluated // when using mutable values, these branches will mutate the final result // rather than make a throw-away computation. @@ -938,38 +1367,63 @@ func (fold *evalFold) ID() int64 { // Exec implements the InterpretableV2 interface method. func (fold *evalFold) Exec(frame *ExecutionFrame) ref.Val { - // Initialize the folder interface f := newFolder(fold, frame) - defer releaseFolder(f) - foldRange := fold.iterRange.Exec(frame) + + // Early return if the folder is an error or unknown. if types.IsUnknownOrError(foldRange) { + releaseFolder(f) return foldRange } - if fold.iterVar2 != "" { - var foldable traits.Foldable - switch r := foldRange.(type) { - case traits.Mapper: - foldable = types.ToFoldableMap(r) - case traits.Lister: - foldable = types.ToFoldableList(r) - default: - return types.NewErrWithNodeID(fold.ID(), "unsupported comprehension range type: %T", foldRange) - } - foldable.Fold(f) - return f.evalResult() + + // Short-circuit the iteration if the range is empty. + if sizer, ok := foldRange.(traits.Sizer); ok && sizer.Size() == types.IntZero { + res := f.evalResult() + releaseFolder(f) + return res } + // Otherwise, attempt a two variable fold. + var foldable traits.Foldable + switch r := foldRange.(type) { + case traits.Foldable: + foldable = r + if fold.iterVar2 == "" { + _, f.listV1Fold = foldRange.(traits.Lister) + } + case traits.Mapper: + foldable = types.ToFoldableMap(r) + case traits.Lister: + if fold.iterVar2 == "" { + f.listV1Fold = true + } + foldable = types.ToFoldableList(r) + default: + if fold.iterVar2 != "" { + return types.NewErrWithNodeID(fold.ID(), "unsupported comprehension range type: %T", foldRange) + } + } + if foldable != nil { + foldable.Fold(f) + res := f.evalResult() + releaseFolder(f) + return res + } + + // If the value is not foldable or just a single variable, fallback to an iterable fold. if !foldRange.Type().HasTrait(traits.IterableType) { + releaseFolder(f) return types.ValOrErr(foldRange, "got '%T', expected iterable type", foldRange) } iterable := foldRange.(traits.Iterable) - return f.foldIterable(iterable) + res := f.foldIterable(iterable) + releaseFolder(f) + return res } // Eval implements the Interpretable interface method. func (fold *evalFold) Eval(ctx Activation) ref.Val { - return fold.Exec(AsFrame(ctx)) + return EvalActivation(ctx, fold.Exec) } // Optional Interpretable implementations that specialize, subsume, or extend the core evaluation @@ -1002,7 +1456,7 @@ func (e *evalSetMembership) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (e *evalSetMembership) Eval(ctx Activation) ref.Val { - return e.Exec(AsFrame(ctx)) + return EvalActivation(ctx, e.Exec) } // evalWatch is an Interpretable implementation that wraps the execution of a given @@ -1021,7 +1475,7 @@ func (e *evalWatch) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (e *evalWatch) Eval(vars Activation) ref.Val { - return e.Exec(AsFrame(vars)) + return EvalActivation(vars, e.Exec) } // evalWatchAttr describes a watcher of an InterpretableAttribute Interpretable. @@ -1085,7 +1539,7 @@ func (e *evalWatchAttr) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (e *evalWatchAttr) Eval(vars Activation) ref.Val { - return e.Exec(AsFrame(vars)) + return EvalActivation(vars, e.Exec) } // evalWatchConstQual observes the qualification of an object using a constant boolean, int, @@ -1221,7 +1675,7 @@ func (e *evalWatchConst) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (e *evalWatchConst) Eval(vars Activation) ref.Val { - return e.Exec(AsFrame(vars)) + return EvalActivation(vars, e.Exec) } // evalExhaustiveOr is just like evalOr, but does not short-circuit argument evaluation. @@ -1248,10 +1702,14 @@ func (or *evalExhaustiveOr) Exec(frame *ExecutionFrame) ref.Val { isTrue = true } if !ok && !isTrue { - isUnk := false - unk, isUnk = types.MaybeMergeUnknowns(val, unk) - if !isUnk && err == nil { - if types.IsError(val) { + if unkVal, isUnk := val.(*types.Unknown); isUnk { + if unk == nil { + unk = unkVal + } else { + unk = types.MergeUnknowns(unk, unkVal) + } + } else if err == nil { + if isError(val) { err = val } else { err = types.MaybeNoSuchOverloadErr(val) @@ -1273,7 +1731,7 @@ func (or *evalExhaustiveOr) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val { - return or.Exec(AsFrame(ctx)) + return EvalActivation(ctx, or.Exec) } // evalExhaustiveAnd is just like evalAnd, but does not short-circuit argument evaluation. @@ -1300,10 +1758,14 @@ func (and *evalExhaustiveAnd) Exec(frame *ExecutionFrame) ref.Val { isFalse = true } if !ok && !isFalse { - isUnk := false - unk, isUnk = types.MaybeMergeUnknowns(val, unk) - if !isUnk && err == nil { - if types.IsError(val) { + if unkVal, isUnk := val.(*types.Unknown); isUnk { + if unk == nil { + unk = unkVal + } else { + unk = types.MergeUnknowns(unk, unkVal) + } + } else if err == nil { + if isError(val) { err = val } else { err = types.MaybeNoSuchOverloadErr(val) @@ -1325,7 +1787,7 @@ func (and *evalExhaustiveAnd) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val { - return and.Exec(AsFrame(ctx)) + return EvalActivation(ctx, and.Exec) } // evalExhaustiveConditional is like evalConditional, but does not short-circuit argument @@ -1364,7 +1826,7 @@ func (cond *evalExhaustiveConditional) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable interface method. func (cond *evalExhaustiveConditional) Eval(ctx Activation) ref.Val { - return cond.Exec(AsFrame(ctx)) + return EvalActivation(ctx, cond.Exec) } // evalAttr evaluates an Attribute value. @@ -1401,15 +1863,23 @@ func (a *evalAttr) Adapter() types.Adapter { // Exec implements the InterpretableV2 interface method. func (a *evalAttr) Exec(frame *ExecutionFrame) ref.Val { v, err := a.attr.Resolve(frame) + var res ref.Val if err != nil { - return types.LabelErrNode(a.ID(), types.WrapErr(err)) + res = types.LabelErrNode(a.ID(), types.WrapErr(err)) + } else { + res = a.adapter.NativeToValue(v) } - return a.adapter.NativeToValue(v) + if _, isCond := a.attr.(*conditionalAttribute); !isCond { + if costs := frame.CostTracker(); costs != nil { + costs.EvalAttribute(a.ID(), false, res) + } + } + return res } // Eval implements the Interpretable interface method. func (a *evalAttr) Eval(ctx Activation) ref.Val { - return a.Exec(AsFrame(ctx)) + return EvalActivation(ctx, a.Exec) } // Qualify proxies to the Attribute's Qualify method. @@ -1460,7 +1930,7 @@ func (c *evalWatchConstructor) Exec(frame *ExecutionFrame) ref.Val { // Eval implements the Interpretable Eval function. func (c *evalWatchConstructor) Eval(vars Activation) ref.Val { - return c.Exec(AsFrame(vars)) + return EvalActivation(vars, c.Exec) } func invalidOptionalEntryInit(field any, value ref.Val) ref.Val { @@ -1508,6 +1978,7 @@ type folder struct { mutableValue bool interrupted bool computeResult bool + listV1Fold bool } func (f *folder) foldIterable(iterable traits.Iterable) ref.Val { @@ -1516,12 +1987,17 @@ func (f *folder) foldIterable(iterable traits.Iterable) ref.Val { f.iterVar1Val = it.Next() cond := f.cond.Exec(f.frame) - condBool, ok := cond.(types.Bool) - if f.interrupted || (!f.exhaustive && ok && condBool != types.True) { + if f.interrupted { + return f.evalResult() + } + if !f.exhaustive && cond != types.True { + if isError(cond) { + f.interrupted = true + } return f.evalResult() } - // Update the accumulation value and check for eval interuption. + // Update the accumulation value and check for eval interruption. f.accuVal = f.step.Exec(f.frame) f.initialized = true if f.interruptable && f.frame.CheckInterrupt() { @@ -1532,22 +2008,36 @@ func (f *folder) foldIterable(iterable traits.Iterable) ref.Val { return f.evalResult() } +// FoldKeyOnly reports whether the folder only consumes the entry key (e.g. 1-variable map +// comprehensions where iterVar2 is empty and listV1Fold is false). +func (f *folder) FoldKeyOnly() bool { + return f.iterVar2 == "" && !f.listV1Fold +} + // FoldEntry will either fold comprehension v1 style macros if iterVar2 is unset, or comprehension v2 style // macros if both the iterVar and iterVar2 are set to non-empty strings. func (f *folder) FoldEntry(key, val any) bool { - // Default to referencing both values. - f.iterVar1Val = key - f.iterVar2Val = val + if f.listV1Fold { + f.iterVar1Val = val + } else { + f.iterVar1Val = key + f.iterVar2Val = val + } // Terminate evaluation if evaluation is interrupted or the condition is not true and exhaustive // eval is not enabled. cond := f.cond.Exec(f.frame) - condBool, ok := cond.(types.Bool) - if f.interrupted || (!f.exhaustive && ok && condBool != types.True) { + if f.interrupted { + return false + } + if !f.exhaustive && cond != types.True { + if isError(cond) { + f.interrupted = true + } return false } - // Update the accumulation value and check for eval interuption. + // Update the accumulation value and check for eval interruption. f.accuVal = f.step.Exec(f.frame) f.initialized = true if f.interruptable && f.frame.CheckInterrupt() { @@ -1565,7 +2055,10 @@ func (f *folder) ResolveName(name string) (any, bool) { if !f.initialized { f.initialized = true initVal := f.accu.Exec(f.frame.parent) - if !f.exhaustive { + // Only substitute a mutable accumulator when the planner has proven that the + // accumulator cannot be aliased, and when evaluation is not exhaustive since + // exhaustive evaluation would apply in-place updates from untaken branches. + if f.mutableAccu && !f.exhaustive { if l, isList := initVal.(traits.Lister); isList && l.Size() == types.IntZero { initVal = types.NewMutableList(f.adapter) f.mutableValue = true @@ -1581,11 +2074,9 @@ func (f *folder) ResolveName(name string) (any, bool) { } if !f.computeResult { if name == f.iterVar { - f.iterVar1Val = f.adapter.NativeToValue(f.iterVar1Val) return f.iterVar1Val, true } if name == f.iterVar2 { - f.iterVar2Val = f.adapter.NativeToValue(f.iterVar2Val) return f.iterVar2Val, true } } @@ -1602,11 +2093,27 @@ func (f *folder) Unwrap() Activation { return f.frame.parent } +// IsLocalVariable reports whether the variable name is locally bound by the folder scope. +func (f *folder) IsLocalVariable(name string) bool { + if name == f.accuVar { + return true + } + if !f.computeResult && (name == f.iterVar || name == f.iterVar2) { + return true + } + if varHolder, ok := f.Parent().(localVariableHolder); ok { + if varHolder.IsLocalVariable(name) { + return true + } + } + return false +} + // UnknownAttributePatterns implements the PartialActivation interface returning the unknown patterns // if they were provided to the input activation, or an empty set if the proxied activation is not partial. func (f *folder) UnknownAttributePatterns() []*AttributePattern { - if pv, ok := f.frame.parent.Activation.(partialActivationConverter); ok { - if partial, isPartial := pv.AsPartialActivation(); isPartial { + if f.frame.parent != nil { + if partial, isPartial := f.frame.parent.AsPartialActivation(); isPartial { return partial.UnknownAttributePatterns() } } @@ -1614,8 +2121,8 @@ func (f *folder) UnknownAttributePatterns() []*AttributePattern { } func (f *folder) AsPartialActivation() (PartialActivation, bool) { - if pv, ok := f.frame.parent.Activation.(partialActivationConverter); ok { - if _, isPartial := pv.AsPartialActivation(); isPartial { + if f.frame.parent != nil { + if _, isPartial := f.frame.parent.AsPartialActivation(); isPartial { return f, true } } @@ -1655,6 +2162,7 @@ func (f *folder) reset() { f.mutableValue = false f.interrupted = false f.computeResult = false + f.listV1Fold = false } // InterruptError is a specialized error type used to signal that program evaluation should check @@ -1679,3 +2187,68 @@ var ( }, } ) + +func trackCostCreateList(frame *ExecutionFrame, id int64, res ref.Val) { + if costs := frame.CostTracker(); costs != nil { + costs.CreateList(id, res) + } +} + +func trackCostCreateMap(frame *ExecutionFrame, id int64, res ref.Val) { + if costs := frame.CostTracker(); costs != nil { + costs.CreateMap(id, res) + } +} + +func trackCostCreateStruct(frame *ExecutionFrame, id int64, res ref.Val) { + if costs := frame.CostTracker(); costs != nil { + costs.CreateStruct(id, res) + } +} + +func trackCostQualify(frame *ExecutionFrame, id int64) { + if costs := frame.CostTracker(); costs != nil { + costs.Qualify(id) + } +} + +func isError(val ref.Val) bool { + _, ok := val.(*types.Err) + return ok +} + +func isUnknown(val ref.Val) bool { + _, ok := val.(*types.Unknown) + return ok +} + +func mergeBinaryUnknowns(lVal, rVal ref.Val) *types.Unknown { + lUnk, lIsUnk := lVal.(*types.Unknown) + rUnk, rIsUnk := rVal.(*types.Unknown) + if !lIsUnk { + if !rIsUnk { + return nil + } + return rUnk + } + if !rIsUnk { + return lUnk + } + return types.MergeUnknowns(lUnk, rUnk) +} + +func mergeUnknown(dst *types.Unknown, src ref.Val) *types.Unknown { + if unk, ok := src.(*types.Unknown); ok { + if dst == nil { + return unk + } + return types.MergeUnknowns(dst, unk) + } + return dst +} +func labelErrNode(id int64, val ref.Val) ref.Val { + if err, ok := val.(*types.Err); ok && err.NodeID() == 0 { + return types.LabelErrNode(id, err) + } + return val +} diff --git a/vendor/github.com/google/cel-go/interpreter/interpreter.go b/vendor/cel.dev/cel-go/interpreter/interpreter.go similarity index 91% rename from vendor/github.com/google/cel-go/interpreter/interpreter.go rename to vendor/cel.dev/cel-go/interpreter/interpreter.go index 38174aff691..da642440d96 100644 --- a/vendor/github.com/google/cel-go/interpreter/interpreter.go +++ b/vendor/cel.dev/cel-go/interpreter/interpreter.go @@ -20,10 +20,10 @@ package interpreter import ( "errors" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // PlannerOption configures the program plan options during interpretable setup. @@ -50,7 +50,7 @@ type StatefulObserver interface { 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(AsFrame(activation)) + // The observe method is expected to do the equivalent of GetState(frame) // to find the metadata that needs to be updated upon invocation. Observe(Activation, int64, any, ref.Val) } @@ -76,6 +76,10 @@ const ( // CostLimitExceeded indicates that the operation was cancelled in response to the actual cost limit being // exceeded. CostLimitExceeded + + // MemoryLimitExceeded indicates that the operation was cancelled in response to the peak memory limit + // being exceeded. + MemoryLimitExceeded ) // evalStateOption configures the evalStateFactory behavior. @@ -101,7 +105,6 @@ func EvalStateObserver(opts ...evalStateOption) PlannerOption { return nil, errors.New("eval state factory not configured") } p.observers = append(p.observers, et) - p.decorators = append(p.decorators, decObserveEval(et.Observe)) return p, nil } } @@ -113,6 +116,12 @@ type activationWrapper interface { Unwrap() Activation } +// localVariableHolder identifies an Activation scope that holds local variables and supports testing +// whether a variable name is local to this scope. +type localVariableHolder interface { + IsLocalVariable(name string) bool +} + // evalStateFactory holds a reference to a factory function that produces an EvalState instance. type evalStateFactory struct { factory func() EvalState @@ -121,6 +130,9 @@ type evalStateFactory struct { // 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(frame *ExecutionFrame) (any, error) { + if frame.ctx != nil && frame.ctx.state != nil { + return frame.ctx.state, nil + } state := et.factory() if frame.ctx == nil { frame.ctx = evalContextPool.Get().(*evalContext) @@ -139,7 +151,10 @@ func (et *evalStateFactory) GetState(frame *ExecutionFrame) any { // 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) { - frame := AsFrame(vars) + frame, ok := vars.(*ExecutionFrame) + if !ok { + return + } if frame.ctx == nil || frame.ctx.state == nil { return } @@ -218,6 +233,11 @@ func CompileRegexConstants(regexOptimizations ...*RegexOptimization) PlannerOpti return CustomDecoratorV2(decRegexOptimizer(regexOptimizations...)) } +// RegexProgramSizeLimit caps the maximum regex program plan size permitted during evaluation. +func RegexProgramSizeLimit(limit int) PlannerOption { + return CustomDecoratorV2(decRegexProgramSizeLimit(limit)) +} + type exprInterpreter struct { dispatcher Dispatcher container *containers.Container diff --git a/vendor/github.com/google/cel-go/interpreter/optimizations.go b/vendor/cel.dev/cel-go/interpreter/optimizations.go similarity index 94% rename from vendor/github.com/google/cel-go/interpreter/optimizations.go rename to vendor/cel.dev/cel-go/interpreter/optimizations.go index 2fc87e693b0..c6478fa72c8 100644 --- a/vendor/github.com/google/cel-go/interpreter/optimizations.go +++ b/vendor/cel.dev/cel-go/interpreter/optimizations.go @@ -17,8 +17,8 @@ package interpreter import ( "regexp" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // MatchesRegexOptimization optimizes the 'matches' standard library function by compiling the regex pattern and diff --git a/vendor/github.com/google/cel-go/interpreter/planner.go b/vendor/cel.dev/cel-go/interpreter/planner.go similarity index 77% rename from vendor/github.com/google/cel-go/interpreter/planner.go rename to vendor/cel.dev/cel-go/interpreter/planner.go index 95fd34551c8..81e8a542272 100644 --- a/vendor/github.com/google/cel-go/interpreter/planner.go +++ b/vendor/cel.dev/cel-go/interpreter/planner.go @@ -18,13 +18,18 @@ import ( "fmt" "strings" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/containers" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/containers" + "cel.dev/cel-go/common/functions" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) +// mapInsertFunction is the name of the internal function used by comprehension macros to insert +// key-value pairs into a map accumulator. +const mapInsertFunction = "cel.@mapInsert" + // newPlanner creates an interpretablePlanner which references a Dispatcher, TypeProvider, // TypeAdapter, Container, and CheckedExpr value. These pieces of data are used to resolve // functions, types, and namespaced identifiers at plan time rather than at runtime since @@ -50,15 +55,16 @@ func newPlanner(disp Dispatcher, // planner is an implementation of the interpretablePlanner interface. type planner struct { - disp Dispatcher - provider types.Provider - adapter types.Adapter - attrFactory AttributeFactory - container *containers.Container - refMap map[int64]*ast.ReferenceInfo - typeMap map[int64]*types.Type - decorators []InterpretableDecoratorV2 - observers []StatefulObserver + disp Dispatcher + provider types.Provider + adapter types.Adapter + attrFactory AttributeFactory + container *containers.Container + refMap map[int64]*ast.ReferenceInfo + typeMap map[int64]*types.Type + decorators []InterpretableDecoratorV2 + observers []StatefulObserver + costTrackerFactory func() (*CostTracker, error) } type planBuilder struct { @@ -73,17 +79,38 @@ type planBuilder struct { // such as state-tracking, expression re-write, and possibly efficient thread-safe memoization of // repeated expressions. func (p *planner) Plan(expr ast.Expr) (InterpretableV2, error) { + if len(p.observers) != 0 { + // A single decorator reports to every observer. One decorator per observer would not + // work, since the second decorator would find the node already wrapped by the first and + // leave it alone, silently dropping the second observer's observations. + p.decorators = append(p.decorators, decObserveEval(observeAll(p.observers))) + } pb := &planBuilder{planner: p, localVars: make(map[string]int)} i, err := pb.plan(expr) if err != nil { return nil, err } + if p.costTrackerFactory != nil { + i = &costTrackingInterpretable{InterpretableV2: i, factory: p.costTrackerFactory} + } if len(p.observers) == 0 { return i, nil } return &ObservableInterpretable{InterpretableV2: i, observers: p.observers}, nil } +// observeAll returns an EvalObserver which reports each observation to all of the observers. +func observeAll(observers []StatefulObserver) EvalObserver { + if len(observers) == 1 { + return observers[0].Observe + } + return func(vars Activation, id int64, programStep any, value ref.Val) { + for _, o := range observers { + o.Observe(vars, id, programStep, value) + } + } +} + func (p *planBuilder) plan(expr ast.Expr) (InterpretableV2, error) { switch expr.Kind() { case ast.CallKind: @@ -281,6 +308,14 @@ func (p *planBuilder) planCall(expr ast.Expr) (InterpretableV2, error) { if fnDef == nil { fnDef, _ = p.disp.FindOverload(fnName) } + // Async overloads are planned into an evalAsyncFunc regardless of arity. + if fnDef != nil && fnDef.Async != nil { + return p.planCallAsync(expr, fnName, oName, fnDef, args) + } + // Late-bound overloads are planned into an evalLateBoundFunc regardless of arity. + if fnDef != nil && fnDef.LateBound { + return p.planCallLateBound(expr, fnName, oName, fnDef, target != nil, args) + } switch argCount { case 0: return p.planCallZero(expr, fnName, oName, fnDef) @@ -303,6 +338,46 @@ func (p *planBuilder) planCall(expr ast.Expr) (InterpretableV2, error) { } } +// planCallLateBound generates a late-bound callable Interpretable. +func (p *planBuilder) planCallLateBound(expr ast.Expr, + function string, + overload string, + impl *functions.Overload, + memberStyle bool, + args []InterpretableV2) (InterpretableV2, error) { + if impl == nil || !impl.LateBound { + return nil, fmt.Errorf("no such overload: %s()", function) + } + return &evalLateBoundFunc{ + id: expr.ID(), + function: function, + overload: overload, + args: args, + trait: impl.OperandTrait, + nonStrict: impl.NonStrict, + member: memberStyle, + dispatch: impl.LateBoundDispatch, + }, nil +} + +// planCallAsync generates an asynchronous callable Interpretable. +func (p *planBuilder) planCallAsync(expr ast.Expr, + function string, + overload string, + impl *functions.Overload, + args []InterpretableV2) (InterpretableV2, error) { + if impl == nil || impl.Async == nil { + return nil, fmt.Errorf("no such overload: %s()", function) + } + return &evalAsyncFunc{ + id: expr.ID(), + function: function, + overload: overload, + args: args, + impl: impl.Async, + }, nil +} + // planCallZero generates a zero-arity callable Interpretable. func (p *planBuilder) planCallZero(expr ast.Expr, function string, @@ -625,19 +700,117 @@ func (p *planBuilder) planComprehension(expr ast.Expr) (InterpretableV2, error) } p.popLocalVars(fold.AccuVar()) return &evalFold{ - id: expr.ID(), - accuVar: fold.AccuVar(), - accu: accu, - iterVar: fold.IterVar(), - iterVar2: fold.IterVar2(), - iterRange: iterRange, - cond: cond, - step: step, - result: result, - adapter: p.adapter, + id: expr.ID(), + accuVar: fold.AccuVar(), + accu: accu, + iterVar: fold.IterVar(), + iterVar2: fold.IterVar2(), + iterRange: iterRange, + cond: cond, + step: step, + result: result, + adapter: p.adapter, + mutableAccu: isMutableAccuSafe(fold), }, nil } +// isMutableAccuSafe reports whether the comprehension accumulator may be backed by a mutable +// list or map during evaluation. +// +// The checks mirror cel-cpp's IsOptimizableListAppend and IsOptimizableMapInsert in +// eval/compiler/flat_expr_builder.cc: the comprehension must have the shape of a standard +// list-building (map/filter/transformList) or map-building (transformMap/transformMapEntry) +// macro expansion. +// +// In addition to the cel-cpp checks, the accumulator variable must be non-empty, must not be +// shadowed by either of the iteration variables, and must begin with '@'. Since '@' cannot +// appear in an identifier written in CEL source, for ASTs produced by the parser and macros this +// ensures that the accumulator is only referenced where the macro expansion placed it and cannot +// be observed or modified by user expressions within the comprehension, e.g. via a legacy +// accumulator name like `__result__` or a custom macro which uses a user-visible accumulator +// name. Hand-crafted ASTs are assumed to be trusted input. +func isMutableAccuSafe(fold ast.ComprehensionExpr) bool { + accuVar := fold.AccuVar() + if !strings.HasPrefix(accuVar, "@") || accuVar == fold.IterVar() || accuVar == fold.IterVar2() { + return false + } + return isOptimizableListAppend(fold) || isOptimizableMapInsert(fold) +} + +// isOptimizableListAppend mirrors cel-cpp's IsOptimizableListAppend. +// +// Returns whether this comprehension appears to be a standard map/filter macro implementation. +func isOptimizableListAppend(fold ast.ComprehensionExpr) bool { + accuVar := fold.AccuVar() + if accuVar == "" || !isIdentNamed(fold.Result(), accuVar) { + return false + } + accuInit := fold.AccuInit() + if accuInit.Kind() != ast.ListKind || accuInit.AsList().Size() != 0 { + return false + } + step := fold.LoopStep() + if step.Kind() != ast.CallKind { + return false + } + // Macro loop_step for a filter() will contain a ternary: + // filter ? accu_var + [elem] : accu_var + // Macro loop_step for a map() will contain a list concat operation: + // accu_var + [elem] + call := step.AsCall() + if call.FunctionName() == operators.Conditional && len(call.Args()) == 3 { + branch := call.Args()[1] + if branch.Kind() != ast.CallKind { + return false + } + call = branch.AsCall() + } + args := call.Args() + return call.FunctionName() == operators.Add && + len(args) == 2 && + isIdentNamed(args[0], accuVar) && + args[1].Kind() == ast.ListKind && + args[1].AsList().Size() == 1 +} + +// isOptimizableMapInsert mirrors cel-cpp's IsOptimizableMapInsert. +// +// Returns whether this comprehension appears to be a macro implementation for map +// transformations. +func isOptimizableMapInsert(fold ast.ComprehensionExpr) bool { + if fold.IterVar() == "" || fold.IterVar2() == "" { + return false + } + accuVar := fold.AccuVar() + if accuVar == "" || !isIdentNamed(fold.Result(), accuVar) { + return false + } + if fold.AccuInit().Kind() != ast.MapKind || fold.AccuInit().AsMap().Size() != 0 { + return false + } + step := fold.LoopStep() + if step.Kind() != ast.CallKind { + return false + } + call := step.AsCall() + if call.FunctionName() == operators.Conditional && len(call.Args()) == 3 { + branch := call.Args()[1] + if branch.Kind() != ast.CallKind { + return false + } + call = branch.AsCall() + } + args := call.Args() + return call.FunctionName() == mapInsertFunction && + (len(args) == 2 || len(args) == 3) && + isIdentNamed(args[0], accuVar) +} + +// isIdentNamed reports whether the expression is an identifier with the given name. +func isIdentNamed(e ast.Expr, name string) bool { + return e != nil && e.Kind() == ast.IdentKind && e.AsIdent() == name +} + // planConst generates a constant valued Interpretable. func (p *planBuilder) planConst(expr ast.Expr) (InterpretableV2, error) { return NewConstValue(expr.ID(), expr.AsLiteral()), nil diff --git a/vendor/github.com/google/cel-go/interpreter/prune.go b/vendor/cel.dev/cel-go/interpreter/prune.go similarity index 98% rename from vendor/github.com/google/cel-go/interpreter/prune.go rename to vendor/cel.dev/cel-go/interpreter/prune.go index 1662c1c1b3a..9f55f1a8115 100644 --- a/vendor/github.com/google/cel-go/interpreter/prune.go +++ b/vendor/cel.dev/cel-go/interpreter/prune.go @@ -15,12 +15,12 @@ package interpreter import ( - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/overloads" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/common/types/traits" ) type astPruner struct { diff --git a/vendor/cel.dev/cel-go/interpreter/runtimecost.go b/vendor/cel.dev/cel-go/interpreter/runtimecost.go new file mode 100644 index 00000000000..e974f4f14fa --- /dev/null +++ b/vendor/cel.dev/cel-go/interpreter/runtimecost.go @@ -0,0 +1,136 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interpreter + +import ( + "errors" + + "cel.dev/cel-go/common/cost" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// WARNING: Any changes to cost calculations in this file require a corresponding change in checker/cost.go + +type ( + // ActualCostEstimator provides function call cost estimations at runtime + // + // Deprecated: use cost.ActualCostEstimator + ActualCostEstimator = cost.ActualCostEstimator + + // FunctionTracker computes the actual cost of evaluating the functions with the given arguments and result. + // + // Deprecated: use cost.FunctionTracker + FunctionTracker = cost.FunctionTracker + + // CostTrackerOption configures the behavior of CostTracker objects. + // + // Deprecated: use cost.CostTrackerOption + CostTrackerOption = cost.TrackerOption + + // CostTracker represents the information needed for tracking runtime cost. + // + // Deprecated: use cost.CostTracker + CostTracker = cost.Tracker +) + +// CostTrackerLimit sets the runtime limit on the evaluation cost during execution and will terminate the expression +// evaluation if the limit is exceeded. +// +// Deprecated: use cost.CostTrackerLimit +func CostTrackerLimit(limit uint64) CostTrackerOption { + return cost.TrackerLimit(limit) +} + +// PresenceTestHasCost determines whether presence testing has a cost of one or zero. +// Defaults to presence test has a cost of one. +// +// Deprecated: use cost.CostTrackerPresenceTestHasCost +func PresenceTestHasCost(hasCost bool) CostTrackerOption { + return cost.TrackerPresenceTestHasCost(hasCost) +} + +// OverloadCostTracker binds an overload ID to a runtime FunctionTracker implementation. +// +// Deprecated: use cost.OverloadCostTracker +func OverloadCostTracker(overloadID string, fnTracker FunctionTracker) CostTrackerOption { + return cost.OverloadTracker(overloadID, fnTracker) +} + +// NewCostTracker creates a new CostTracker with a given estimator and a set of functional CostTrackerOption values. +// +// Deprecated: use cost.NewCostTracker +func NewCostTracker(estimator ActualCostEstimator, opts ...CostTrackerOption) (*CostTracker, error) { + evalCancelHandler := cost.TrackerLimitExceededHandler(func() { + panic(EvalCancelledError{Cause: CostLimitExceeded, Message: "operation cancelled: actual cost limit exceeded"}) + }) + allOpts := make([]cost.TrackerOption, 0, len(opts)+1) + allOpts = append(allOpts, evalCancelHandler) + allOpts = append(allOpts, opts...) + return cost.NewTracker(estimator, allOpts...) +} + +// CostTrackPlanOption modifies the cost tracking factory associated with the CostObserver. +type CostTrackPlanOption func(*costTrackerFactory) *costTrackerFactory + +type costTrackPlanOption = CostTrackPlanOption + +// CostTrackerFactory configures the factory method to generate a new cost-tracker per-evaluation. +func CostTrackerFactory(factory func() (*CostTracker, error)) CostTrackPlanOption { + return func(fac *costTrackerFactory) *costTrackerFactory { + fac.factory = factory + return fac + } +} + +// CostObserver provides an observer that tracks runtime cost. +func CostObserver(opts ...CostTrackPlanOption) PlannerOption { + ct := &costTrackerFactory{} + for _, o := range opts { + ct = o(ct) + } + return func(p *planner) (*planner, error) { + if ct.factory == nil { + return nil, errors.New("cost tracker factory not configured") + } + p.costTrackerFactory = ct.factory + return p, nil + } +} + +// costTrackerFactory holds a factory for producing new CostTracker instances on each Eval call. +type costTrackerFactory struct { + factory func() (*CostTracker, error) +} + +type costTrackingInterpretable struct { + InterpretableV2 + factory func() (*CostTracker, error) +} + +func (c *costTrackingInterpretable) Exec(frame *ExecutionFrame) ref.Val { + if frame.CostTracker() == nil { + tracker, err := c.factory() + if err != nil { + return types.NewErr("cost tracker factory: %v", err) + } + frame.SetCostTracker(tracker) + } + return c.InterpretableV2.Exec(frame) +} + +func (c *costTrackingInterpretable) Eval(ctx Activation) ref.Val { + return EvalActivation(ctx, c.Exec) +} diff --git a/vendor/cel.dev/cel-go/interpreter/runtimememory.go b/vendor/cel.dev/cel-go/interpreter/runtimememory.go new file mode 100644 index 00000000000..c4d8a02ac7b --- /dev/null +++ b/vendor/cel.dev/cel-go/interpreter/runtimememory.go @@ -0,0 +1,105 @@ +// 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 ( + "errors" + + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" +) + +// memoryTrackPlanOption modifies the memory tracking factory associated with the MemoryObserver. +type memoryTrackPlanOption func(*memoryTrackerFactory) *memoryTrackerFactory + +// MemoryTrackerFactory configures the factory method to generate a new memory-tracker +// per-evaluation. +func MemoryTrackerFactory(factory func() (*types.MemoryTracker, error)) memoryTrackPlanOption { + return func(fac *memoryTrackerFactory) *memoryTrackerFactory { + fac.factory = factory + return fac + } +} + +// MemoryObserver provides an observer that tracks runtime peak memory. +func MemoryObserver(opts ...memoryTrackPlanOption) PlannerOption { + mt := &memoryTrackerFactory{} + for _, o := range opts { + mt = o(mt) + } + return func(p *planner) (*planner, error) { + if mt.factory == nil { + return nil, errors.New("memory tracker factory not configured") + } + p.observers = append(p.observers, mt) + return p, nil + } +} + +// memoryTrackerFactory holds a factory for producing new MemoryTracker instances on each Eval call. +type memoryTrackerFactory struct { + factory func() (*types.MemoryTracker, error) +} + +// InitState produces a MemoryTracker and bundles it into the ExecutionFrame in a way which is +// not visible to expression evaluation. +func (mt *memoryTrackerFactory) InitState(frame *ExecutionFrame) (any, error) { + if frame.ctx != nil && frame.ctx.memory != nil { + return frame.ctx.memory, nil + } + tracker, err := mt.factory() + if err != nil { + return nil, err + } + if frame.ctx == nil { + frame.ctx = evalContextPool.Get().(*evalContext) + } + frame.ctx.memory = tracker + return tracker, nil +} + +// GetState extracts the MemoryTracker from the ExecutionFrame. +func (mt *memoryTrackerFactory) GetState(frame *ExecutionFrame) any { + if frame == nil || frame.ctx == nil { + return nil + } + return frame.ctx.memory +} + +// Observe records the peak memory watermarks associated with each evaluation step. +// +// Watermarks are observed at the points where values materialize during evaluation: resolved +// attributes, function call results, constructed aggregate literals, and comprehension results. +// Since every intermediate Interpretable is observed, the inputs to a call contribute to the +// peak at the expression nodes which produced them; constants are part of the program image +// rather than runtime-materialized memory and are not observed. +func (mt *memoryTrackerFactory) Observe(vars Activation, id int64, programStep any, val ref.Val) { + frame, ok := vars.(*ExecutionFrame) + if !ok || frame.ctx == nil || frame.ctx.memory == nil { + return + } + tracker := frame.ctx.memory + switch programStep.(type) { + case InterpretableAttribute, InterpretableConst, InterpretableCall, InterpretableConstructor, *evalFold: + if frame.parent != nil { + tracker.Sample(id, val) + } else { + tracker.Track(val) + } + if tracker.ExceedsLimit() { + panic(EvalCancelledError{Cause: MemoryLimitExceeded, Message: "operation cancelled: memory limit exceeded"}) + } + } +} diff --git a/vendor/github.com/google/cel-go/parser/BUILD.bazel b/vendor/cel.dev/cel-go/parser/BUILD.bazel similarity index 86% rename from vendor/github.com/google/cel-go/parser/BUILD.bazel rename to vendor/cel.dev/cel-go/parser/BUILD.bazel index 97bc9bd4344..138b9ee5294 100644 --- a/vendor/github.com/google/cel-go/parser/BUILD.bazel +++ b/vendor/cel.dev/cel-go/parser/BUILD.bazel @@ -7,16 +7,19 @@ package( go_library( name = "go_default_library", srcs = [ + "antlr_parser.go", "errors.go", "helper.go", "input.go", + "lexer.go", "macro.go", "options.go", "parser.go", + "pratt_parser.go", "unescape.go", "unparser.go", ], - importpath = "github.com/google/cel-go/parser", + importpath = "cel.dev/cel-go/parser", visibility = ["//visibility:public"], deps = [ "//common:go_default_library", @@ -38,6 +41,7 @@ go_test( size = "small", srcs = [ "helper_test.go", + "lexer_test.go", "parser_test.go", "unescape_test.go", "unparser_test.go", @@ -46,8 +50,11 @@ go_test( ":go_default_library", ], deps = [ + "//common:go_default_library", "//common/ast:go_default_library", "//common/debug:go_default_library", + "//common/operators:go_default_library", + "//common/runes:go_default_library", "//common/types:go_default_library", "//parser/gen:go_default_library", "//test:go_default_library", diff --git a/vendor/github.com/google/cel-go/parser/parser.go b/vendor/cel.dev/cel-go/parser/antlr_parser.go similarity index 80% rename from vendor/github.com/google/cel-go/parser/parser.go rename to vendor/cel.dev/cel-go/parser/antlr_parser.go index d1567b5ff07..e755b7ac7b6 100644 --- a/vendor/github.com/google/cel-go/parser/parser.go +++ b/vendor/cel.dev/cel-go/parser/antlr_parser.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google LLC +// 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. @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package parser declares an expression parser with support for macro -// expansion. package parser import ( @@ -25,83 +23,34 @@ import ( antlr "github.com/antlr4-go/antlr/v4" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/runes" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/parser/gen" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/runes" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/parser/gen" ) -// Parser encapsulates the context necessary to perform parsing for different expressions. -type Parser struct { +// antlrParser encapsulates the context necessary to perform ANTLR parsing for different expressions. +type antlrParser struct { options } -// NewParser builds and returns a new Parser using the provided options. -func NewParser(opts ...Option) (*Parser, error) { - p := &Parser{} - p.enableHiddenAccumulatorName = true - p.enableIdentEscapeSyntax = true - for _, opt := range opts { - if err := opt(&p.options); err != nil { - return nil, err - } - } - if p.errorReportingLimit == 0 { - p.errorReportingLimit = 100 - } - if p.maxRecursionDepth == 0 { - p.maxRecursionDepth = 250 - } - if p.maxRecursionDepth == -1 { - p.maxRecursionDepth = int((^uint(0)) >> 1) - } - if p.errorRecoveryTokenLookaheadLimit == 0 { - p.errorRecoveryTokenLookaheadLimit = 256 - } - if p.errorRecoveryLimit == 0 { - p.errorRecoveryLimit = 30 - } - if p.errorRecoveryLimit == -1 { - p.errorRecoveryLimit = int((^uint(0)) >> 1) - } - if p.expressionSizeCodePointLimit == 0 { - p.expressionSizeCodePointLimit = 100_000 - } - if p.expressionSizeCodePointLimit == -1 { - p.expressionSizeCodePointLimit = int((^uint(0)) >> 1) - } - // Bool is false by default, so populateMacroCalls will be false by default - return p, nil -} - -// mustNewParser does the work of NewParser and panics if an error occurs. -// -// This function is only intended for internal use and is for backwards compatibility in Parse and -// ParseWithMacros, where we know the options will result in an error. -func mustNewParser(opts ...Option) *Parser { - p, err := NewParser(opts...) - if err != nil { - panic(err) - } - return p -} - // Parse parses the expression represented by source and returns the result. -func (p *Parser) Parse(source common.Source) (*ast.AST, *common.Errors) { +func (p *antlrParser) Parse(source common.Source) (*ast.AST, *common.Errors) { errs := common.NewErrors(source) accu := AccumulatorName if p.enableHiddenAccumulatorName { accu = HiddenAccumulatorName } fac := ast.NewExprFactoryWithAccumulator(accu) - impl := parser{ + impl := antlrVisitor{ errors: &parseErrors{errs}, exprFactory: fac, helper: newParserHelper(source, fac), macros: p.macros, maxRecursionDepth: p.maxRecursionDepth, + maxExpressionNodeCount: p.maxExpressionNodeCount, errorReportingLimit: p.errorReportingLimit, errorRecoveryLimit: p.errorRecoveryLimit, errorRecoveryLookaheadTokenLimit: p.errorRecoveryTokenLookaheadLimit, @@ -125,32 +74,6 @@ func (p *Parser) Parse(source common.Source) (*ast.AST, *common.Errors) { return ast.NewAST(out, impl.helper.getSourceInfo()), errs } -// reservedIds are not legal to use as variables. We exclude them post-parse, as they *are* valid -// field names for protos, and it would complicate the grammar to distinguish the cases. -var reservedIds = map[string]struct{}{ - "as": {}, - "break": {}, - "const": {}, - "continue": {}, - "else": {}, - "false": {}, - "for": {}, - "function": {}, - "if": {}, - "import": {}, - "in": {}, - "let": {}, - "loop": {}, - "package": {}, - "namespace": {}, - "null": {}, - "return": {}, - "true": {}, - "var": {}, - "void": {}, - "while": {}, -} - func unescapeIdent(in string) (string, error) { if len(in) <= 2 { return "", errors.New("invalid escaped identifier: underflow") @@ -158,28 +81,6 @@ func unescapeIdent(in string) (string, error) { return in[1 : len(in)-1], nil } -// normalizeIdent returns the interpreted identifier. -func (p *parser) normalizeIdent(ctx gen.IEscapeIdentContext) (string, error) { - switch ident := ctx.(type) { - case *gen.SimpleIdentifierContext: - return ident.GetId().GetText(), nil - case *gen.EscapedIdentifierContext: - if !p.enableIdentEscapeSyntax { - return "", errors.New("unsupported syntax: '`'") - } - return unescapeIdent(ident.GetId().GetText()) - } - return "", errors.New("unsupported ident kind") -} - -// Parse converts a source input a parsed expression. -// This function calls ParseWithMacros with AllMacros. -// -// Deprecated: Use NewParser().Parse() instead. -func Parse(source common.Source) (*ast.AST, *common.Errors) { - return mustNewParser(Macros(AllMacros...)).Parse(source) -} - type recursionError struct { message string } @@ -310,7 +211,7 @@ func (rl *recoveryLimitErrorStrategy) checkAttempts(recognizer antlr.Parser) { var _ antlr.ErrorStrategy = &recoveryLimitErrorStrategy{} -type parser struct { +type antlrVisitor struct { gen.BaseCELVisitor errors *parseErrors exprFactory ast.ExprFactory @@ -319,6 +220,7 @@ type parser struct { recursionDepth int errorReports int maxRecursionDepth int + maxExpressionNodeCount int errorReportingLimit int errorRecoveryLimit int errorRecoveryLookaheadTokenLimit int @@ -328,9 +230,23 @@ type parser struct { enableIdentEscapeSyntax bool } -var _ gen.CELVisitor = (*parser)(nil) +var _ gen.CELVisitor = (*antlrVisitor)(nil) -func (p *parser) parse(expr runes.Buffer, desc string) ast.Expr { +// normalizeIdent returns the interpreted identifier. +func (p *antlrVisitor) normalizeIdent(ctx gen.IEscapeIdentContext) (string, error) { + switch ident := ctx.(type) { + case *gen.SimpleIdentifierContext: + return ident.GetId().GetText(), nil + case *gen.EscapedIdentifierContext: + if !p.enableIdentEscapeSyntax { + return "", errors.New("unsupported syntax: '`'") + } + return unescapeIdent(ident.GetId().GetText()) + } + return "", errors.New("unsupported ident kind") +} + +func (p *antlrVisitor) parse(expr runes.Buffer, desc string) ast.Expr { lexer := gen.NewCELLexer(newCharStream(expr, desc)) lexer.RemoveErrorListeners() lexer.AddErrorListener(p) @@ -373,7 +289,7 @@ func (p *parser) parse(expr runes.Buffer, desc string) ast.Expr { } // Visitor implementations. -func (p *parser) Visit(tree antlr.ParseTree) any { +func (p *antlrVisitor) Visit(tree antlr.ParseTree) any { t := unnest(tree) switch tree := t.(type) { case *gen.StartContext: @@ -458,16 +374,15 @@ func (p *parser) Visit(tree antlr.ParseTree) any { return p.reportError(common.NoLocation, "unknown parse element encountered: %s", txt) } return p.helper.newExpr(common.NoLocation) - } // Visit a parse tree produced by CELParser#start. -func (p *parser) VisitStart(ctx *gen.StartContext) any { +func (p *antlrVisitor) VisitStart(ctx *gen.StartContext) any { return p.Visit(ctx.Expr()) } // Visit a parse tree produced by CELParser#expr. -func (p *parser) VisitExpr(ctx *gen.ExprContext) any { +func (p *antlrVisitor) VisitExpr(ctx *gen.ExprContext) any { result := p.Visit(ctx.GetE()).(ast.Expr) if ctx.GetOp() == nil { return result @@ -479,7 +394,7 @@ func (p *parser) VisitExpr(ctx *gen.ExprContext) any { } // Visit a parse tree produced by CELParser#conditionalOr. -func (p *parser) VisitConditionalOr(ctx *gen.ConditionalOrContext) any { +func (p *antlrVisitor) VisitConditionalOr(ctx *gen.ConditionalOrContext) any { result := p.Visit(ctx.GetE()).(ast.Expr) l := p.newLogicManager(operators.LogicalOr, result) rest := ctx.GetE1() @@ -495,7 +410,7 @@ func (p *parser) VisitConditionalOr(ctx *gen.ConditionalOrContext) any { } // Visit a parse tree produced by CELParser#conditionalAnd. -func (p *parser) VisitConditionalAnd(ctx *gen.ConditionalAndContext) any { +func (p *antlrVisitor) VisitConditionalAnd(ctx *gen.ConditionalAndContext) any { result := p.Visit(ctx.GetE()).(ast.Expr) l := p.newLogicManager(operators.LogicalAnd, result) rest := ctx.GetE1() @@ -511,7 +426,7 @@ func (p *parser) VisitConditionalAnd(ctx *gen.ConditionalAndContext) any { } // Visit a parse tree produced by CELParser#relation. -func (p *parser) VisitRelation(ctx *gen.RelationContext) any { +func (p *antlrVisitor) VisitRelation(ctx *gen.RelationContext) any { opText := "" if ctx.GetOp() != nil { opText = ctx.GetOp().GetText() @@ -526,7 +441,7 @@ func (p *parser) VisitRelation(ctx *gen.RelationContext) any { } // Visit a parse tree produced by CELParser#calc. -func (p *parser) VisitCalc(ctx *gen.CalcContext) any { +func (p *antlrVisitor) VisitCalc(ctx *gen.CalcContext) any { opText := "" if ctx.GetOp() != nil { opText = ctx.GetOp().GetText() @@ -540,12 +455,12 @@ func (p *parser) VisitCalc(ctx *gen.CalcContext) any { return p.reportError(ctx, "operator not found") } -func (p *parser) VisitUnary(ctx *gen.UnaryContext) any { +func (p *antlrVisitor) VisitUnary(ctx *gen.UnaryContext) any { return p.helper.newLiteralString(ctx, "<>") } // Visit a parse tree produced by CELParser#LogicalNot. -func (p *parser) VisitLogicalNot(ctx *gen.LogicalNotContext) any { +func (p *antlrVisitor) VisitLogicalNot(ctx *gen.LogicalNotContext) any { if len(ctx.GetOps())%2 == 0 { return p.Visit(ctx.Member()) } @@ -554,7 +469,7 @@ func (p *parser) VisitLogicalNot(ctx *gen.LogicalNotContext) any { return p.globalCallOrMacro(opID, operators.LogicalNot, target) } -func (p *parser) VisitNegate(ctx *gen.NegateContext) any { +func (p *antlrVisitor) VisitNegate(ctx *gen.NegateContext) any { if len(ctx.GetOps())%2 == 0 { return p.Visit(ctx.Member()) } @@ -564,7 +479,7 @@ func (p *parser) VisitNegate(ctx *gen.NegateContext) any { } // VisitSelect visits a parse tree produced by CELParser#Select. -func (p *parser) VisitSelect(ctx *gen.SelectContext) any { +func (p *antlrVisitor) VisitSelect(ctx *gen.SelectContext) any { operand := p.Visit(ctx.Member()).(ast.Expr) // Handle the error case where no valid identifier is specified. if ctx.GetId() == nil || ctx.GetOp() == nil { @@ -588,7 +503,7 @@ func (p *parser) VisitSelect(ctx *gen.SelectContext) any { } // VisitMemberCall visits a parse tree produced by CELParser#MemberCall. -func (p *parser) VisitMemberCall(ctx *gen.MemberCallContext) any { +func (p *antlrVisitor) VisitMemberCall(ctx *gen.MemberCallContext) any { operand := p.Visit(ctx.Member()).(ast.Expr) // Handle the error case where no valid identifier is specified. if ctx.GetId() == nil { @@ -600,7 +515,7 @@ func (p *parser) VisitMemberCall(ctx *gen.MemberCallContext) any { } // Visit a parse tree produced by CELParser#Index. -func (p *parser) VisitIndex(ctx *gen.IndexContext) any { +func (p *antlrVisitor) VisitIndex(ctx *gen.IndexContext) any { target := p.Visit(ctx.Member()).(ast.Expr) // Handle the error case where no valid identifier is specified. if ctx.GetOp() == nil { @@ -619,7 +534,7 @@ func (p *parser) VisitIndex(ctx *gen.IndexContext) any { } // Visit a parse tree produced by CELParser#CreateMessage. -func (p *parser) VisitCreateMessage(ctx *gen.CreateMessageContext) any { +func (p *antlrVisitor) VisitCreateMessage(ctx *gen.CreateMessageContext) any { messageName := "" for _, id := range ctx.GetIds() { if len(messageName) != 0 { @@ -636,7 +551,7 @@ func (p *parser) VisitCreateMessage(ctx *gen.CreateMessageContext) any { } // Visit a parse tree of field initializers. -func (p *parser) VisitIFieldInitializerList(ctx gen.IFieldInitializerListContext) any { +func (p *antlrVisitor) VisitIFieldInitializerList(ctx gen.IFieldInitializerListContext) any { if ctx == nil || ctx.GetFields() == nil { // This is the result of a syntax error handled elswhere, return empty. return []ast.EntryExpr{} @@ -673,7 +588,7 @@ func (p *parser) VisitIFieldInitializerList(ctx gen.IFieldInitializerListContext } // Visit a parse tree produced by CELParser#Ident. -func (p *parser) VisitIdent(ctx *gen.IdentContext) any { +func (p *antlrVisitor) VisitIdent(ctx *gen.IdentContext) any { identName := "" if ctx.GetLeadingDot() != nil { identName = "." @@ -692,7 +607,7 @@ func (p *parser) VisitIdent(ctx *gen.IdentContext) any { } // Visit a parse tree produced by CELParser#GlobalCallContext. -func (p *parser) VisitGlobalCall(ctx *gen.GlobalCallContext) any { +func (p *antlrVisitor) VisitGlobalCall(ctx *gen.GlobalCallContext) any { identName := "" if ctx.GetLeadingDot() != nil { identName = "." @@ -709,18 +624,17 @@ func (p *parser) VisitGlobalCall(ctx *gen.GlobalCallContext) any { identName += id opID := p.helper.id(ctx.GetOp()) return p.globalCallOrMacro(opID, identName, p.visitExprList(ctx.GetArgs())...) - } // Visit a parse tree produced by CELParser#CreateList. -func (p *parser) VisitCreateList(ctx *gen.CreateListContext) any { +func (p *antlrVisitor) VisitCreateList(ctx *gen.CreateListContext) any { listID := p.helper.id(ctx.GetOp()) elems, optionals := p.visitListInit(ctx.GetElems()) return p.helper.newList(listID, elems, optionals...) } // Visit a parse tree produced by CELParser#CreateStruct. -func (p *parser) VisitCreateStruct(ctx *gen.CreateStructContext) any { +func (p *antlrVisitor) VisitCreateStruct(ctx *gen.CreateStructContext) any { structID := p.helper.id(ctx.GetOp()) entries := []ast.EntryExpr{} if ctx.GetEntries() != nil { @@ -730,7 +644,7 @@ func (p *parser) VisitCreateStruct(ctx *gen.CreateStructContext) any { } // Visit a parse tree produced by CELParser#mapInitializerList. -func (p *parser) VisitMapInitializerList(ctx *gen.MapInitializerListContext) any { +func (p *antlrVisitor) VisitMapInitializerList(ctx *gen.MapInitializerListContext) any { if ctx == nil || ctx.GetKeys() == nil { // This is the result of a syntax error handled elswhere, return empty. return []ast.EntryExpr{} @@ -760,7 +674,7 @@ func (p *parser) VisitMapInitializerList(ctx *gen.MapInitializerListContext) any } // Visit a parse tree produced by CELParser#Int. -func (p *parser) VisitInt(ctx *gen.IntContext) any { +func (p *antlrVisitor) VisitInt(ctx *gen.IntContext) any { text := ctx.GetTok().GetText() base := 10 if strings.HasPrefix(text, "0x") { @@ -778,7 +692,7 @@ func (p *parser) VisitInt(ctx *gen.IntContext) any { } // Visit a parse tree produced by CELParser#Uint. -func (p *parser) VisitUint(ctx *gen.UintContext) any { +func (p *antlrVisitor) VisitUint(ctx *gen.UintContext) any { text := ctx.GetTok().GetText() // trim the 'u' designator included in the uint literal. text = text[:len(text)-1] @@ -795,7 +709,7 @@ func (p *parser) VisitUint(ctx *gen.UintContext) any { } // Visit a parse tree produced by CELParser#Double. -func (p *parser) VisitDouble(ctx *gen.DoubleContext) any { +func (p *antlrVisitor) VisitDouble(ctx *gen.DoubleContext) any { txt := ctx.GetTok().GetText() if ctx.GetSign() != nil { txt = ctx.GetSign().GetText() + txt @@ -805,44 +719,43 @@ func (p *parser) VisitDouble(ctx *gen.DoubleContext) any { return p.reportError(ctx, "invalid double literal") } return p.helper.newLiteralDouble(ctx, f) - } // Visit a parse tree produced by CELParser#String. -func (p *parser) VisitString(ctx *gen.StringContext) any { +func (p *antlrVisitor) VisitString(ctx *gen.StringContext) any { s := p.unquote(ctx, ctx.GetTok().GetText(), false) return p.helper.newLiteralString(ctx, s) } // Visit a parse tree produced by CELParser#Bytes. -func (p *parser) VisitBytes(ctx *gen.BytesContext) any { +func (p *antlrVisitor) VisitBytes(ctx *gen.BytesContext) any { b := []byte(p.unquote(ctx, ctx.GetTok().GetText()[1:], true)) return p.helper.newLiteralBytes(ctx, b) } // Visit a parse tree produced by CELParser#BoolTrue. -func (p *parser) VisitBoolTrue(ctx *gen.BoolTrueContext) any { +func (p *antlrVisitor) VisitBoolTrue(ctx *gen.BoolTrueContext) any { return p.helper.newLiteralBool(ctx, true) } // Visit a parse tree produced by CELParser#BoolFalse. -func (p *parser) VisitBoolFalse(ctx *gen.BoolFalseContext) any { +func (p *antlrVisitor) VisitBoolFalse(ctx *gen.BoolFalseContext) any { return p.helper.newLiteralBool(ctx, false) } // Visit a parse tree produced by CELParser#Null. -func (p *parser) VisitNull(ctx *gen.NullContext) any { +func (p *antlrVisitor) VisitNull(ctx *gen.NullContext) any { return p.helper.exprFactory.NewLiteral(p.helper.newID(ctx), types.NullValue) } -func (p *parser) visitExprList(ctx gen.IExprListContext) []ast.Expr { +func (p *antlrVisitor) visitExprList(ctx gen.IExprListContext) []ast.Expr { if ctx == nil { return []ast.Expr{} } return p.visitSlice(ctx.GetE()) } -func (p *parser) visitListInit(ctx gen.IListInitContext) ([]ast.Expr, []int32) { +func (p *antlrVisitor) visitListInit(ctx gen.IListInitContext) ([]ast.Expr, []int32) { if ctx == nil { return []ast.Expr{}, []int32{} } @@ -866,7 +779,7 @@ func (p *parser) visitListInit(ctx gen.IListInitContext) ([]ast.Expr, []int32) { return result, optionals } -func (p *parser) visitSlice(expressions []gen.IExprContext) []ast.Expr { +func (p *antlrVisitor) visitSlice(expressions []gen.IExprContext) []ast.Expr { if expressions == nil { return []ast.Expr{} } @@ -878,7 +791,7 @@ func (p *parser) visitSlice(expressions []gen.IExprContext) []ast.Expr { return result } -func (p *parser) unquote(ctx any, value string, isBytes bool) string { +func (p *antlrVisitor) unquote(ctx any, value string, isBytes bool) string { text, err := unescape(value, isBytes) if err != nil { p.reportError(ctx, "%s", err.Error()) @@ -887,14 +800,14 @@ func (p *parser) unquote(ctx any, value string, isBytes bool) string { return text } -func (p *parser) newLogicManager(function string, term ast.Expr) *logicManager { +func (p *antlrVisitor) newLogicManager(function string, term ast.Expr) *logicManager { if p.enableVariadicOperatorASTs { return newVariadicLogicManager(p.exprFactory, function, term) } return newBalancingLogicManager(p.exprFactory, function, term) } -func (p *parser) reportError(ctx any, format string, args ...any) ast.Expr { +func (p *antlrVisitor) reportError(ctx any, format string, args ...any) ast.Expr { var location common.Location err := p.helper.newExpr(ctx) switch c := ctx.(type) { @@ -909,7 +822,7 @@ func (p *parser) reportError(ctx any, format string, args ...any) ast.Expr { } // ANTLR Parse listener implementations -func (p *parser) SyntaxError(recognizer antlr.Recognizer, offendingSymbol any, line, column int, msg string, e antlr.RecognitionException) { +func (p *antlrVisitor) SyntaxError(recognizer antlr.Recognizer, offendingSymbol any, line, column int, msg string, e antlr.RecognitionException) { offset := p.helper.sourceInfo.ComputeOffset(int32(line), int32(column)) l := p.helper.getLocationByOffset(offset) // Hack to keep existing error messages consistent with previous versions of CEL when a reserved word @@ -930,33 +843,33 @@ func (p *parser) SyntaxError(recognizer antlr.Recognizer, offendingSymbol any, l } } -func (p *parser) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, exact bool, ambigAlts *antlr.BitSet, configs *antlr.ATNConfigSet) { +func (p *antlrVisitor) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, exact bool, ambigAlts *antlr.BitSet, configs *antlr.ATNConfigSet) { // Intentional } -func (p *parser) ReportAttemptingFullContext(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, conflictingAlts *antlr.BitSet, configs *antlr.ATNConfigSet) { +func (p *antlrVisitor) ReportAttemptingFullContext(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, conflictingAlts *antlr.BitSet, configs *antlr.ATNConfigSet) { // Intentional } -func (p *parser) ReportContextSensitivity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex, prediction int, configs *antlr.ATNConfigSet) { +func (p *antlrVisitor) ReportContextSensitivity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex, prediction int, configs *antlr.ATNConfigSet) { // Intentional } -func (p *parser) globalCallOrMacro(exprID int64, function string, args ...ast.Expr) ast.Expr { +func (p *antlrVisitor) globalCallOrMacro(exprID int64, function string, args ...ast.Expr) ast.Expr { if expr, found := p.expandMacro(exprID, function, nil, args...); found { return expr } return p.helper.newGlobalCall(exprID, function, args...) } -func (p *parser) receiverCallOrMacro(exprID int64, function string, target ast.Expr, args ...ast.Expr) ast.Expr { +func (p *antlrVisitor) receiverCallOrMacro(exprID int64, function string, target ast.Expr, args ...ast.Expr) ast.Expr { if expr, found := p.expandMacro(exprID, function, target, args...); found { return expr } return p.helper.newReceiverCall(exprID, function, target, args...) } -func (p *parser) expandMacro(exprID int64, function string, target ast.Expr, args ...ast.Expr) (ast.Expr, bool) { +func (p *antlrVisitor) expandMacro(exprID int64, function string, target ast.Expr, args ...ast.Expr) (ast.Expr, bool) { macro, found := p.macros[makeMacroKey(function, len(args), target != nil)] if !found { macro, found = p.macros[makeVarArgMacroKey(function, target != nil)] @@ -964,11 +877,21 @@ func (p *parser) expandMacro(exprID int64, function string, target ast.Expr, arg return nil, false } } + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } eh := exprHelperPool.Get().(*exprHelper) defer exprHelperPool.Put(eh) eh.parserHelper = p.helper eh.id = exprID expr, err := macro.Expander()(eh, target, args) + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } // An error indicates that the macro was matched, but the arguments were not well-formed. if err != nil { loc := err.Location @@ -990,14 +913,14 @@ func (p *parser) expandMacro(exprID int64, function string, target ast.Expr, arg return expr, true } -func (p *parser) checkAndIncrementRecursionDepth() { +func (p *antlrVisitor) checkAndIncrementRecursionDepth() { p.recursionDepth++ if p.recursionDepth > p.maxRecursionDepth { panic(&recursionError{message: "max recursion depth exceeded"}) } } -func (p *parser) decrementRecursionDepth() { +func (p *antlrVisitor) decrementRecursionDepth() { p.recursionDepth-- } diff --git a/vendor/github.com/google/cel-go/parser/errors.go b/vendor/cel.dev/cel-go/parser/errors.go similarity index 97% rename from vendor/github.com/google/cel-go/parser/errors.go rename to vendor/cel.dev/cel-go/parser/errors.go index c3cec01a8d9..968dbe82baf 100644 --- a/vendor/github.com/google/cel-go/parser/errors.go +++ b/vendor/cel.dev/cel-go/parser/errors.go @@ -15,7 +15,7 @@ package parser import ( - "github.com/google/cel-go/common" + "cel.dev/cel-go/common" ) // parseErrors is a specialization of Errors. diff --git a/vendor/github.com/google/cel-go/parser/gen/BUILD.bazel b/vendor/cel.dev/cel-go/parser/gen/BUILD.bazel similarity index 90% rename from vendor/github.com/google/cel-go/parser/gen/BUILD.bazel rename to vendor/cel.dev/cel-go/parser/gen/BUILD.bazel index 3efed87b70f..6c0187d58c3 100644 --- a/vendor/github.com/google/cel-go/parser/gen/BUILD.bazel +++ b/vendor/cel.dev/cel-go/parser/gen/BUILD.bazel @@ -19,7 +19,7 @@ go_library( "CEL.tokens", "CELLexer.tokens", ], - importpath = "github.com/google/cel-go/parser/gen", + importpath = "cel.dev/cel-go/parser/gen", deps = [ "@com_github_antlr4_go_antlr_v4//:go_default_library", ], diff --git a/vendor/github.com/google/cel-go/parser/gen/CEL.g4 b/vendor/cel.dev/cel-go/parser/gen/CEL.g4 similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/CEL.g4 rename to vendor/cel.dev/cel-go/parser/gen/CEL.g4 diff --git a/vendor/github.com/google/cel-go/parser/gen/CEL.interp b/vendor/cel.dev/cel-go/parser/gen/CEL.interp similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/CEL.interp rename to vendor/cel.dev/cel-go/parser/gen/CEL.interp diff --git a/vendor/github.com/google/cel-go/parser/gen/CEL.tokens b/vendor/cel.dev/cel-go/parser/gen/CEL.tokens similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/CEL.tokens rename to vendor/cel.dev/cel-go/parser/gen/CEL.tokens diff --git a/vendor/github.com/google/cel-go/parser/gen/CELLexer.interp b/vendor/cel.dev/cel-go/parser/gen/CELLexer.interp similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/CELLexer.interp rename to vendor/cel.dev/cel-go/parser/gen/CELLexer.interp diff --git a/vendor/github.com/google/cel-go/parser/gen/CELLexer.tokens b/vendor/cel.dev/cel-go/parser/gen/CELLexer.tokens similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/CELLexer.tokens rename to vendor/cel.dev/cel-go/parser/gen/CELLexer.tokens diff --git a/vendor/github.com/google/cel-go/parser/gen/cel_base_listener.go b/vendor/cel.dev/cel-go/parser/gen/cel_base_listener.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/cel_base_listener.go rename to vendor/cel.dev/cel-go/parser/gen/cel_base_listener.go diff --git a/vendor/github.com/google/cel-go/parser/gen/cel_base_visitor.go b/vendor/cel.dev/cel-go/parser/gen/cel_base_visitor.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/cel_base_visitor.go rename to vendor/cel.dev/cel-go/parser/gen/cel_base_visitor.go diff --git a/vendor/github.com/google/cel-go/parser/gen/cel_lexer.go b/vendor/cel.dev/cel-go/parser/gen/cel_lexer.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/cel_lexer.go rename to vendor/cel.dev/cel-go/parser/gen/cel_lexer.go diff --git a/vendor/github.com/google/cel-go/parser/gen/cel_listener.go b/vendor/cel.dev/cel-go/parser/gen/cel_listener.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/cel_listener.go rename to vendor/cel.dev/cel-go/parser/gen/cel_listener.go diff --git a/vendor/github.com/google/cel-go/parser/gen/cel_parser.go b/vendor/cel.dev/cel-go/parser/gen/cel_parser.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/cel_parser.go rename to vendor/cel.dev/cel-go/parser/gen/cel_parser.go diff --git a/vendor/github.com/google/cel-go/parser/gen/cel_visitor.go b/vendor/cel.dev/cel-go/parser/gen/cel_visitor.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/cel_visitor.go rename to vendor/cel.dev/cel-go/parser/gen/cel_visitor.go diff --git a/vendor/github.com/google/cel-go/parser/gen/doc.go b/vendor/cel.dev/cel-go/parser/gen/doc.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/doc.go rename to vendor/cel.dev/cel-go/parser/gen/doc.go diff --git a/vendor/github.com/google/cel-go/parser/gen/generate.sh b/vendor/cel.dev/cel-go/parser/gen/generate.sh similarity index 100% rename from vendor/github.com/google/cel-go/parser/gen/generate.sh rename to vendor/cel.dev/cel-go/parser/gen/generate.sh diff --git a/vendor/github.com/google/cel-go/parser/helper.go b/vendor/cel.dev/cel-go/parser/helper.go similarity index 92% rename from vendor/github.com/google/cel-go/parser/helper.go rename to vendor/cel.dev/cel-go/parser/helper.go index f960be20ed5..8281b2d4210 100644 --- a/vendor/github.com/google/cel-go/parser/helper.go +++ b/vendor/cel.dev/cel-go/parser/helper.go @@ -19,10 +19,10 @@ import ( antlr "github.com/antlr4-go/antlr/v4" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) type parserHelper struct { @@ -45,6 +45,10 @@ func (p *parserHelper) getSourceInfo() *ast.SourceInfo { return p.sourceInfo } +func (p *parserHelper) expressionCount() int64 { + return p.nextID - 1 +} + func (p *parserHelper) newLiteral(ctx any, value ref.Val) ast.Expr { return p.exprFactory.NewLiteral(p.newID(ctx), value) } @@ -148,6 +152,18 @@ func (p *parserHelper) newExpr(ctx any) ast.Expr { return p.exprFactory.NewUnspecifiedExpr(p.newID(ctx)) } +// computeOffset converts a 0-based character offset from the local source content to +// the corresponding absolute character offset in the parent SourceInfo (e.g. for RelativeSource). +// If the location cannot be resolved or the source is nil, the original offset is returned. +func (p *parserHelper) computeOffset(offset int32) int32 { + if p.source != nil { + if loc, found := p.source.OffsetLocation(offset); found { + return p.sourceInfo.ComputeOffsetAbsolute(int32(loc.Line()), int32(loc.Column())) + } + } + return offset +} + func (p *parserHelper) id(ctx any) int64 { var offset ast.OffsetRange switch c := ctx.(type) { @@ -158,6 +174,9 @@ func (p *parserHelper) id(ctx any) int64 { case antlr.Token: offset.Start = p.sourceInfo.ComputeOffset(int32(c.GetLine()), int32(c.GetColumn())) offset.Stop = offset.Start + int32(len(c.GetText())) + case token: + offset.Start = p.computeOffset(c.start) + offset.Stop = p.computeOffset(c.end) case common.Location: offset.Start = p.sourceInfo.ComputeOffsetAbsolute(int32(c.Line()), int32(c.Column())) offset.Stop = offset.Start @@ -173,6 +192,28 @@ func (p *parserHelper) id(ctx any) int64 { return id } +func (p *parserHelper) idFromOffsets(start, stop int32) int64 { + id := p.nextID + p.sourceInfo.SetOffsetRange(id, ast.OffsetRange{Start: p.computeOffset(start), Stop: p.computeOffset(stop)}) + p.nextID++ + return id +} + +func (p *parserHelper) allocID() int64 { + id := p.nextID + p.nextID++ + return id +} + +func (p *parserHelper) setTokenLocation(id int64, tok token) { + if tok.start >= 0 { + p.sourceInfo.SetOffsetRange(id, ast.OffsetRange{ + Start: p.computeOffset(tok.start), + Stop: p.computeOffset(tok.end), + }) + } +} + func (p *parserHelper) deleteID(id int64) { p.sourceInfo.ClearOffsetRange(id) if id == p.nextID-1 { diff --git a/vendor/github.com/google/cel-go/parser/input.go b/vendor/cel.dev/cel-go/parser/input.go similarity index 98% rename from vendor/github.com/google/cel-go/parser/input.go rename to vendor/cel.dev/cel-go/parser/input.go index 44792455d8b..9ccccd0fc10 100644 --- a/vendor/github.com/google/cel-go/parser/input.go +++ b/vendor/cel.dev/cel-go/parser/input.go @@ -17,7 +17,7 @@ package parser import ( antlr "github.com/antlr4-go/antlr/v4" - "github.com/google/cel-go/common/runes" + "cel.dev/cel-go/common/runes" ) type charStream struct { diff --git a/vendor/cel.dev/cel-go/parser/lexer.go b/vendor/cel.dev/cel-go/parser/lexer.go new file mode 100644 index 00000000000..c696270f972 --- /dev/null +++ b/vendor/cel.dev/cel-go/parser/lexer.go @@ -0,0 +1,664 @@ +// 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 parser + +import ( + "cel.dev/cel-go/common/runes" +) + +type tokenKind int + +const ( + tokError tokenKind = iota + tokEnd + tokWhitespace + tokComment + + // Keywords + tokNull + tokFalse + tokTrue + tokIn + tokReservedWord + + // Literals + tokInt + tokUint + tokFloat + tokString + tokBytes + + // Identifiers + tokIdent + + // Delimiters + tokLeftBracket // [ + tokRightBracket // ] + tokLeftBrace // { + tokRightBrace // } + tokLeftParen // ( + tokRightParen // ) + + // Operators + tokDot // . + tokComma // , + tokMinus // - + tokPlus // + + tokAsterisk // * + tokSlash // / + tokPercent // % + tokQuestion // ? + tokColon // : + tokExclamation // ! + tokEqual // = + tokEqualEqual // == + tokExclamationEqual // != + tokLess // < + tokLessEqual // <= + tokGreater // > + tokGreaterEqual // >= + tokLogicalAnd // && + tokLogicalOr // || +) + +func (t tokenKind) String() string { + switch t { + case tokError: + return "error" + case tokEnd: + return "end" + case tokWhitespace: + return "whitespace" + case tokComment: + return "comment" + case tokNull: + return "null" + case tokFalse: + return "false" + case tokTrue: + return "true" + case tokIn: + return "in" + case tokReservedWord: + return "reserved_word" + case tokInt: + return "int" + case tokUint: + return "uint" + case tokFloat: + return "float" + case tokString: + return "string" + case tokBytes: + return "bytes" + case tokIdent: + return "ident" + case tokLeftBracket: + return "[" + case tokRightBracket: + return "]" + case tokLeftBrace: + return "{" + case tokRightBrace: + return "}" + case tokLeftParen: + return "(" + case tokRightParen: + return ")" + case tokDot: + return "." + case tokComma: + return "," + case tokMinus: + return "-" + case tokPlus: + return "+" + case tokAsterisk: + return "*" + case tokSlash: + return "/" + case tokPercent: + return "%" + case tokQuestion: + return "?" + case tokColon: + return ":" + case tokExclamation: + return "!" + case tokEqual: + return "=" + case tokEqualEqual: + return "==" + case tokExclamationEqual: + return "!=" + case tokLess: + return "<" + case tokLessEqual: + return "<=" + case tokGreater: + return ">" + case tokGreaterEqual: + return ">=" + case tokLogicalAnd: + return "&&" + case tokLogicalOr: + return "||" + default: + return "" + } +} + +type token struct { + kind tokenKind + start int32 + end int32 +} + +type lexerError struct { + start int32 + end int32 + message string +} + +var keywords = map[string]tokenKind{ + "false": tokFalse, + "true": tokTrue, + "null": tokNull, + "in": tokIn, + "as": tokReservedWord, + "break": tokReservedWord, + "const": tokReservedWord, + "continue": tokReservedWord, + "else": tokReservedWord, + "for": tokReservedWord, + "function": tokReservedWord, + "if": tokReservedWord, + "import": tokReservedWord, + "let": tokReservedWord, + "loop": tokReservedWord, + "package": tokReservedWord, + "namespace": tokReservedWord, + "return": tokReservedWord, + "var": tokReservedWord, + "void": tokReservedWord, + "while": tokReservedWord, +} + +func isIdentTrailing(r rune) bool { + return r <= 0x7f && ((r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_') +} + +func isDigit(r rune) bool { + return r >= '0' && r <= '9' +} + +func isHexDigit(r rune) bool { + return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') +} + +func isAlpha(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') +} + +func isPlusOrMinus(r rune) bool { + return r == '+' || r == '-' +} + +// lexer performs fast tokenization of CEL expression source code. +type lexer struct { + content runes.Buffer + length int32 + pos int32 + err lexerError +} + +func newLexer(content runes.Buffer) *lexer { + return &lexer{ + content: content, + length: int32(content.Len()), + pos: 0, + } +} + +func (l *lexer) SavePosition() int32 { + return l.pos +} + +func (l *lexer) RestorePosition(pos int32) { + l.pos = pos + l.err = lexerError{} +} + +func (l *lexer) GetError() lexerError { + return l.err +} + +func (l *lexer) GetPosition() int32 { + return l.pos +} + +func (l *lexer) makeToken(kind tokenKind, start, end int32) token { + return token{kind: kind, start: start, end: end} +} + +func (l *lexer) setError(start, end int32, msg string) token { + l.err = lexerError{start: start, end: end, message: msg} + return token{kind: tokError, start: start, end: end} +} + +func (l *lexer) advance(n int32) { + l.pos += n +} + +func (l *lexer) match(c rune) bool { + return l.pos < l.length && l.content.Get(int(l.pos)) == c +} + +func (l *lexer) matchIgnoreCase(c rune) bool { + if l.pos >= l.length { + return false + } + cp := l.content.Get(int(l.pos)) + if cp <= 0x7f && c <= 0x7f { + cpLower := cp + if cp >= 'A' && cp <= 'Z' { + cpLower += 'a' - 'A' + } + cLower := c + if c >= 'A' && c <= 'Z' { + cLower += 'a' - 'A' + } + return cpLower == cLower + } + return cp == c +} + +func (l *lexer) consume(c rune) bool { + if l.match(c) { + l.advance(1) + return true + } + return false +} + +func (l *lexer) consumeIgnoreCase(c rune) bool { + if l.matchIgnoreCase(c) { + l.advance(1) + return true + } + return false +} + +func (l *lexer) consumeIf(predicate func(rune) bool) bool { + if l.pos < l.length && predicate(l.content.Get(int(l.pos))) { + l.advance(1) + return true + } + return false +} + +func (l *lexer) consumeLine() { + for l.pos < l.length { + if l.content.Get(int(l.pos)) == '\n' { + l.advance(1) + return + } + l.advance(1) + } +} + +func (l *lexer) consumeWhitespace() { + for l.pos < l.length { + c := l.content.Get(int(l.pos)) + switch c { + case '\f', '\n', ' ', '\r', '\t': + l.advance(1) + default: + return + } + } +} + +func (l *lexer) consumeDigits() bool { + advanced := false + for l.pos < l.length { + c := l.content.Get(int(l.pos)) + if !isDigit(c) { + break + } + l.advance(1) + advanced = true + } + return advanced +} + +func (l *lexer) consumeHexDigits() bool { + advanced := false + for l.pos < l.length { + c := l.content.Get(int(l.pos)) + if !isHexDigit(c) { + break + } + l.advance(1) + advanced = true + } + return advanced +} + +func (l *lexer) consumeIntegralSuffix() tokenKind { + if l.consumeIgnoreCase('u') { + return tokUint + } + return tokInt +} + +func (l *lexer) consumeUntilAfter(c rune, isRaw bool) bool { + pos := l.pos + escaped := false + for pos < l.length { + cc := l.content.Get(int(pos)) + if cc == '\n' || cc == '\r' { + l.pos = pos + return false + } + if !isRaw && cc == '\\' { + escaped = !escaped + } else { + if cc == c && !escaped { + l.pos = pos + 1 + return true + } + escaped = false + } + pos++ + } + l.pos = l.length + return false +} + +func (l *lexer) consumeUntilAfterTriple(quote rune, isRaw bool) bool { + pos := l.pos + escaped := false + for pos < l.length { + cc := l.content.Get(int(pos)) + if !isRaw && cc == '\\' { + escaped = !escaped + } else { + if !escaped && pos+3 <= l.length { + if l.content.Get(int(pos)) == quote && + l.content.Get(int(pos+1)) == quote && + l.content.Get(int(pos+2)) == quote { + l.pos = pos + 3 + return true + } + } + escaped = false + } + pos++ + } + l.pos = l.length + return false +} + +func (l *lexer) consumeQuotedIdent() token { + start := l.pos + l.advance(1) + if !l.consumeUntilAfter('`', true) { + return l.setError(start, l.pos, "unterminated quoted identifier") + } + return l.makeToken(tokIdent, start, l.pos) +} + +func (l *lexer) consumeStringLiteral(start int32, quote rune, isBytes, isRaw bool) token { + l.advance(1) + if l.pos+2 <= l.length && l.content.Get(int(l.pos)) == quote && l.content.Get(int(l.pos+1)) == quote { + l.advance(2) + if !l.consumeUntilAfterTriple(quote, isRaw) { + msg := "unterminated string literal" + if isBytes { + msg = "unterminated bytes literal" + } + return l.setError(start, l.pos, msg) + } + kind := tokString + if isBytes { + kind = tokBytes + } + return l.makeToken(kind, start, l.pos) + } + if !l.consumeUntilAfter(quote, isRaw) { + msg := "unterminated string literal" + if isBytes { + msg = "unterminated bytes literal" + } + return l.setError(start, l.pos, msg) + } + kind := tokString + if isBytes { + kind = tokBytes + } + return l.makeToken(kind, start, l.pos) +} + +func (l *lexer) consumePrefixedStringLiteral() (token, bool) { + start := l.pos + if l.pos >= l.length { + return token{}, false + } + c := l.content.Get(int(l.pos)) + isBytes := (c == 'b' || c == 'B') + isRaw := (c == 'r' || c == 'R') + lookahead := int32(1) + if l.pos+1 < l.length { + c2 := l.content.Get(int(l.pos + 1)) + if (isBytes && (c2 == 'r' || c2 == 'R')) || (!isBytes && (c2 == 'b' || c2 == 'B')) { + isBytes = true + isRaw = true + lookahead = 2 + } + } + if l.pos+lookahead < l.length { + quote := l.content.Get(int(l.pos + lookahead)) + if quote == '"' || quote == '\'' { + l.advance(lookahead) + return l.consumeStringLiteral(start, quote, isBytes, isRaw), true + } + } + return token{}, false +} + +func (l *lexer) consumeNumericLiteral() token { + start := l.pos + c := l.content.Get(int(l.pos)) + floatingPoint := false + if c == '.' { + floatingPoint = true + l.advance(1) + if !l.consumeDigits() { + return l.setError(start, l.pos, "floating point literal missing digits after decimal separator") + } + } else { + l.advance(1) + if c == '0' { + if l.consumeIgnoreCase('x') { + if !l.consumeHexDigits() { + return l.setError(start, l.pos, "integral literal missing digits after hexadecimal separator") + } + tokType := l.consumeIntegralSuffix() + return l.makeToken(tokType, start, l.pos) + } + } + _ = l.consumeDigits() + if l.pos < l.length && l.content.Get(int(l.pos)) == '.' && + l.pos+1 < l.length && isDigit(l.content.Get(int(l.pos+1))) { + floatingPoint = true + l.advance(1) + _ = l.consumeDigits() + } + } + if l.consumeIgnoreCase('e') { + floatingPoint = true + _ = l.consumeIf(isPlusOrMinus) + if !l.consumeDigits() { + return l.setError(start, l.pos, "floating point literal missing digits after exponent separator") + } + } + var tokType tokenKind + if floatingPoint { + tokType = tokFloat + } else { + tokType = l.consumeIntegralSuffix() + } + return l.makeToken(tokType, start, l.pos) +} + +func (l *lexer) consumeIdent() token { + start := l.pos + for l.pos < l.length { + c := l.content.Get(int(l.pos)) + if !isIdentTrailing(c) { + break + } + l.advance(1) + } + end := l.pos + word := l.content.Slice(int(start), int(end)) + if kind, ok := keywords[word]; ok { + return l.makeToken(kind, start, end) + } + return l.makeToken(tokIdent, start, end) +} + +// Lex scans and returns the next token from the source. +func (l *lexer) Lex() token { + start := l.pos + if l.pos >= l.length { + return l.makeToken(tokEnd, start, start) + } + c := l.content.Get(int(l.pos)) + switch c { + case '\f', '\t', '\r', '\n', ' ': + l.consumeWhitespace() + return l.makeToken(tokWhitespace, start, l.pos) + case '.': + if l.pos+1 < l.length && isDigit(l.content.Get(int(l.pos+1))) { + return l.consumeNumericLiteral() + } + l.advance(1) + return l.makeToken(tokDot, start, l.pos) + case ',': + l.advance(1) + return l.makeToken(tokComma, start, l.pos) + case '!': + l.advance(1) + if l.consume('=') { + return l.makeToken(tokExclamationEqual, start, l.pos) + } + return l.makeToken(tokExclamation, start, l.pos) + case '?': + l.advance(1) + return l.makeToken(tokQuestion, start, l.pos) + case '(': + l.advance(1) + return l.makeToken(tokLeftParen, start, l.pos) + case ')': + l.advance(1) + return l.makeToken(tokRightParen, start, l.pos) + case '{': + l.advance(1) + return l.makeToken(tokLeftBrace, start, l.pos) + case '}': + l.advance(1) + return l.makeToken(tokRightBrace, start, l.pos) + case '[': + l.advance(1) + return l.makeToken(tokLeftBracket, start, l.pos) + case ']': + l.advance(1) + return l.makeToken(tokRightBracket, start, l.pos) + case '=': + l.advance(1) + if l.consume('=') { + return l.makeToken(tokEqualEqual, start, l.pos) + } + return l.makeToken(tokEqual, start, l.pos) + case '<': + l.advance(1) + if l.consume('=') { + return l.makeToken(tokLessEqual, start, l.pos) + } + return l.makeToken(tokLess, start, l.pos) + case '>': + l.advance(1) + if l.consume('=') { + return l.makeToken(tokGreaterEqual, start, l.pos) + } + return l.makeToken(tokGreater, start, l.pos) + case ':': + l.advance(1) + return l.makeToken(tokColon, start, l.pos) + case '%': + l.advance(1) + return l.makeToken(tokPercent, start, l.pos) + case '+': + l.advance(1) + return l.makeToken(tokPlus, start, l.pos) + case '-': + l.advance(1) + return l.makeToken(tokMinus, start, l.pos) + case '*': + l.advance(1) + return l.makeToken(tokAsterisk, start, l.pos) + case '/': + l.advance(1) + if l.consume('/') { + l.consumeLine() + return l.makeToken(tokComment, start, l.pos) + } + return l.makeToken(tokSlash, start, l.pos) + case '&': + l.advance(1) + if l.consume('&') { + return l.makeToken(tokLogicalAnd, start, l.pos) + } + return l.setError(start, l.pos, "unexpected single '&', expected '&&'") + case '|': + l.advance(1) + if l.consume('|') { + return l.makeToken(tokLogicalOr, start, l.pos) + } + return l.setError(start, l.pos, "unexpected single '|', expected '||'") + case '_': + return l.consumeIdent() + case '`': + return l.consumeQuotedIdent() + case '\'': + return l.consumeStringLiteral(start, '\'', false, false) + case '"': + return l.consumeStringLiteral(start, '"', false, false) + case 'r', 'R', 'b', 'B': + if tok, ok := l.consumePrefixedStringLiteral(); ok { + return tok + } + } + if isDigit(c) { + return l.consumeNumericLiteral() + } + if isAlpha(c) { + return l.consumeIdent() + } + l.advance(1) + return l.setError(start, l.pos, "unexpected character") +} diff --git a/vendor/github.com/google/cel-go/parser/macro.go b/vendor/cel.dev/cel-go/parser/macro.go similarity index 99% rename from vendor/github.com/google/cel-go/parser/macro.go rename to vendor/cel.dev/cel-go/parser/macro.go index 1ef43c4b5ed..b9f53d7baa1 100644 --- a/vendor/github.com/google/cel-go/parser/macro.go +++ b/vendor/cel.dev/cel-go/parser/macro.go @@ -17,11 +17,11 @@ package parser import ( "fmt" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // MacroOpt defines a functional option for configuring macro behavior. diff --git a/vendor/github.com/google/cel-go/parser/options.go b/vendor/cel.dev/cel-go/parser/options.go similarity index 83% rename from vendor/github.com/google/cel-go/parser/options.go rename to vendor/cel.dev/cel-go/parser/options.go index 4eb30f83e05..6fbeb0bd66e 100644 --- a/vendor/github.com/google/cel-go/parser/options.go +++ b/vendor/cel.dev/cel-go/parser/options.go @@ -22,12 +22,15 @@ type options struct { errorRecoveryTokenLookaheadLimit int errorRecoveryLimit int expressionSizeCodePointLimit int + maxExpressionNodeCount int macros map[string]Macro populateMacroCalls bool enableOptionalSyntax bool enableVariadicOperatorASTs bool enableIdentEscapeSyntax bool + enableCallEscapeSyntax bool enableHiddenAccumulatorName bool + enablePrattParser bool } // Option configures the behavior of the parser. @@ -97,6 +100,18 @@ func ExpressionSizeCodePointLimit(expressionSizeCodePointLimit int) Option { } } +// MaxExpressionNodeCount limits the maximum number of expression nodes that may be emitted by the parser, +// including nodes created by macro expansion. +func MaxExpressionNodeCount(limit int) Option { + return func(opts *options) error { + if limit < -1 { + return fmt.Errorf("max expression node count must be greater than or equal to -1: %d", limit) + } + opts.maxExpressionNodeCount = limit + return nil + } +} + // Macros adds the given macros to the parser. func Macros(macros ...Macro) Option { return func(opts *options) error { @@ -161,3 +176,20 @@ func EnableVariadicOperatorASTs(varArgASTs bool) Option { return nil } } + +// EnablePrattParser enables the Pratt parser implementation instead of the ANTLR parser. +func EnablePrattParser(enablePrattParser bool) Option { + return func(opts *options) error { + opts.enablePrattParser = enablePrattParser + return nil + } +} + +// EnableCallEscapeSyntax enables backtick (`) escaped function and method call identifiers, +// as well as internal identifiers with '@' characters. +func EnableCallEscapeSyntax(enableCallEscapeSyntax bool) Option { + return func(opts *options) error { + opts.enableCallEscapeSyntax = enableCallEscapeSyntax + return nil + } +} diff --git a/vendor/cel.dev/cel-go/parser/parser.go b/vendor/cel.dev/cel-go/parser/parser.go new file mode 100644 index 00000000000..8fe906a0b2a --- /dev/null +++ b/vendor/cel.dev/cel-go/parser/parser.go @@ -0,0 +1,127 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package parser declares an expression parser with support for macro +// expansion. +package parser + +import ( + "math" + + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" +) + +// Parser encapsulates the context necessary to perform parsing for different expressions. +type Parser struct { + options +} + +// NewParser builds and returns a new Parser using the provided options. +func NewParser(opts ...Option) (*Parser, error) { + p := &Parser{} + p.enableHiddenAccumulatorName = true + p.enableIdentEscapeSyntax = true + for _, opt := range opts { + if err := opt(&p.options); err != nil { + return nil, err + } + } + if p.errorReportingLimit == 0 { + p.errorReportingLimit = 100 + } + if p.maxRecursionDepth == 0 { + p.maxRecursionDepth = 250 + } + if p.maxRecursionDepth == -1 { + p.maxRecursionDepth = math.MaxInt + } + if p.errorRecoveryTokenLookaheadLimit == 0 { + p.errorRecoveryTokenLookaheadLimit = 256 + } + if p.errorRecoveryLimit == 0 { + p.errorRecoveryLimit = 30 + } + if p.errorRecoveryLimit == -1 { + p.errorRecoveryLimit = math.MaxInt + } + if p.expressionSizeCodePointLimit == 0 { + p.expressionSizeCodePointLimit = 100_000 + } + if p.expressionSizeCodePointLimit == -1 { + p.expressionSizeCodePointLimit = math.MaxInt + } + if p.maxExpressionNodeCount == 0 { + p.maxExpressionNodeCount = 100_000 + } + if p.maxExpressionNodeCount == -1 { + p.maxExpressionNodeCount = math.MaxInt + } + // Bool is false by default, so populateMacroCalls will be false by default + return p, nil +} + +// mustNewParser does the work of NewParser and panics if an error occurs. +// +// This function is only intended for internal use and is for backwards compatibility in Parse and +// ParseWithMacros, where we know the options will result in an error. +func mustNewParser(opts ...Option) *Parser { + p, err := NewParser(opts...) + if err != nil { + panic(err) + } + return p +} + +// Parse parses the expression represented by source and returns the result. +func (p *Parser) Parse(source common.Source) (*ast.AST, *common.Errors) { + if p.enablePrattParser { + return (&prattParser{options: p.options}).Parse(source) + } + return (&antlrParser{options: p.options}).Parse(source) +} + +// reservedIds are not legal to use as variables. We exclude them post-parse, as they *are* valid +// field names for protos, and it would complicate the grammar to distinguish the cases. +var reservedIds = map[string]struct{}{ + "as": {}, + "break": {}, + "const": {}, + "continue": {}, + "else": {}, + "false": {}, + "for": {}, + "function": {}, + "if": {}, + "import": {}, + "in": {}, + "let": {}, + "loop": {}, + "package": {}, + "namespace": {}, + "null": {}, + "return": {}, + "true": {}, + "var": {}, + "void": {}, + "while": {}, +} + +// Parse converts a source input a parsed expression. +// This function calls ParseWithMacros with AllMacros. +// +// Deprecated: Use NewParser().Parse() instead. +func Parse(source common.Source) (*ast.AST, *common.Errors) { + return mustNewParser(Macros(AllMacros...)).Parse(source) +} diff --git a/vendor/cel.dev/cel-go/parser/pratt_parser.go b/vendor/cel.dev/cel-go/parser/pratt_parser.go new file mode 100644 index 00000000000..72a4e1deac2 --- /dev/null +++ b/vendor/cel.dev/cel-go/parser/pratt_parser.go @@ -0,0 +1,1133 @@ +// 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 parser + +import ( + "fmt" + "strconv" + "strings" + + "cel.dev/cel-go/common" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/runes" + "cel.dev/cel-go/common/types" +) + +type binaryOpInfo struct { + precedence int + name string + kind tokenKind +} + +var ( + opLogicalOr = binaryOpInfo{precedence: 1, name: operators.LogicalOr, kind: tokLogicalOr} + opLogicalAnd = binaryOpInfo{precedence: 2, name: operators.LogicalAnd, kind: tokLogicalAnd} + opLess = binaryOpInfo{precedence: 3, name: operators.Less, kind: tokLess} + opLessEqual = binaryOpInfo{precedence: 3, name: operators.LessEquals, kind: tokLessEqual} + opGreater = binaryOpInfo{precedence: 3, name: operators.Greater, kind: tokGreater} + opGreaterEqual = binaryOpInfo{precedence: 3, name: operators.GreaterEquals, kind: tokGreaterEqual} + opEqualEqual = binaryOpInfo{precedence: 3, name: operators.Equals, kind: tokEqualEqual} + opExclamationEqual = binaryOpInfo{precedence: 3, name: operators.NotEquals, kind: tokExclamationEqual} + opIn = binaryOpInfo{precedence: 3, name: operators.In, kind: tokIn} + opPlus = binaryOpInfo{precedence: 4, name: operators.Add, kind: tokPlus} + opMinus = binaryOpInfo{precedence: 4, name: operators.Subtract, kind: tokMinus} + opAsterisk = binaryOpInfo{precedence: 5, name: operators.Multiply, kind: tokAsterisk} + opSlash = binaryOpInfo{precedence: 5, name: operators.Divide, kind: tokSlash} + opPercent = binaryOpInfo{precedence: 5, name: operators.Modulo, kind: tokPercent} + opDefault = binaryOpInfo{precedence: 0, name: "", kind: tokError} + + binaryOpInfoTable = [tokLogicalOr + 1]binaryOpInfo{ + tokLogicalOr: opLogicalOr, + tokLogicalAnd: opLogicalAnd, + tokLess: opLess, + tokLessEqual: opLessEqual, + tokGreater: opGreater, + tokGreaterEqual: opGreaterEqual, + tokEqualEqual: opEqualEqual, + tokExclamationEqual: opExclamationEqual, + tokIn: opIn, + tokPlus: opPlus, + tokMinus: opMinus, + tokAsterisk: opAsterisk, + tokSlash: opSlash, + tokPercent: opPercent, + } +) + +func getBinaryOpInfo(kind tokenKind) binaryOpInfo { + if int(kind) < len(binaryOpInfoTable) { + return binaryOpInfoTable[kind] + } + return opDefault +} + +type prattParserWorker struct { + content runes.Buffer + length int32 + helper *parserHelper + errors *parseErrors + exprFactory ast.ExprFactory + lexer *lexer + currTok token + peekTok token + macros map[string]Macro + recursionDepth int + lastParsedDepth int + recursionLimitExceeded bool + errorCount int + maxRecursionDepth int + maxExpressionNodeCount int + errorReportingLimit int + errorRecoveryLimit int + populateMacroCalls bool + enableOptionalSyntax bool + enableVariadicOperatorASTs bool + enableIdentEscapeSyntax bool + enableCallEscapeSyntax bool +} + +// prattParser encapsulates the context necessary to perform Pratt parsing for different expressions. +type prattParser struct { + options +} + +// Parse parses the expression represented by source using the Pratt parser and returns the result. +func (p *prattParser) Parse(source common.Source) (*ast.AST, *common.Errors) { + errs := common.NewErrors(source) + buf, ok := source.(runes.Buffer) + if !ok { + buf = runes.NewBuffer(source.Content()) + } + if buf.Len() > p.expressionSizeCodePointLimit { + errs.ReportError(common.NoLocation, + "expression code point size exceeds limit: size: %d, limit %d", + buf.Len(), p.expressionSizeCodePointLimit) + return nil, errs + } + accu := AccumulatorName + if p.enableHiddenAccumulatorName { + accu = HiddenAccumulatorName + } + fac := ast.NewExprFactoryWithAccumulator(accu) + pratt := &prattParserWorker{ + content: buf, + length: int32(buf.Len()), + helper: newParserHelper(source, fac), + errors: &parseErrors{errs}, + exprFactory: fac, + lexer: newLexer(buf), + macros: p.macros, + maxRecursionDepth: p.maxRecursionDepth, + maxExpressionNodeCount: p.maxExpressionNodeCount, + errorReportingLimit: p.errorReportingLimit, + errorRecoveryLimit: p.errorRecoveryLimit, + populateMacroCalls: p.populateMacroCalls, + enableOptionalSyntax: p.enableOptionalSyntax, + enableVariadicOperatorASTs: p.enableVariadicOperatorASTs, + enableIdentEscapeSyntax: p.enableIdentEscapeSyntax, + enableCallEscapeSyntax: p.enableCallEscapeSyntax, + } + pratt.initTokenStream() + out := pratt.parse() + if len(errs.GetErrors()) > 0 { + return nil, errs + } + return ast.NewAST(out, pratt.helper.getSourceInfo()), errs +} + +func (p *prattParserWorker) initTokenStream() { + p.currTok = token{kind: tokError, start: 0, end: 0} + p.peekTok = p.nextSignificantToken() +} + +func (p *prattParserWorker) isRecoveryLimitExceeded() bool { + return p.errorCount > p.errorRecoveryLimit +} + +// checkRecursion returns true and trips the recursion limit if depth exceeds the configured +// limit. +// +// The parser bounds two depths separately against maxRecursionDepth: +// +// - recursionDepth tracks syntactic expression nesting: each parenthesized expression, list, +// map or struct element, call argument, index and conditional false branch opens a new +// level. This bounds the parser's Go call-stack recursion (along with iterative runs of +// grouping parentheses). +// - lastParsedDepth tracks the depth of the AST parsed so far, counting binary, conditional, +// field selection, member call and index nodes. Grouping parentheses, unary operators +// (which are folded), logical operators (which are balanced or variadic), and composite or +// call containers (lists, maps, structs and global calls, whose nesting is already bounded +// by recursionDepth) do not add to it. Because operator and selector chains are parsed +// iteratively, this depth is computed bottom-up and checked as each node is built. +func (p *prattParserWorker) checkRecursion(depth int) bool { + if depth > p.maxRecursionDepth { + if !p.recursionLimitExceeded { + p.recursionLimitExceeded = true + p.errors.internalError(fmt.Sprintf("expression recursion limit exceeded: %d", p.maxRecursionDepth)) + p.peekTok = token{kind: tokEnd, start: p.length, end: p.length} + } + return true + } + return false +} + +func (p *prattParserWorker) nextSignificantToken() token { + if p.isRecoveryLimitExceeded() { + return token{kind: tokEnd, start: p.length, end: p.length} + } + for { + tok := p.lexer.Lex() + if tok.kind == tokWhitespace || tok.kind == tokComment { + continue + } + if tok.kind == tokError { + p.reportSyntaxError(tok, "%s", p.lexer.GetError().message) + if p.isRecoveryLimitExceeded() { + return token{kind: tokEnd, start: p.length, end: p.length} + } + } + return tok + } +} + +func (p *prattParserWorker) scanNextSignificantToken() token { + for { + tok := p.lexer.Lex() + if tok.kind == tokWhitespace || tok.kind == tokComment { + continue + } + return tok + } +} + +// isStructCreationAhead checks whether the current identifier is the root of a struct/message +// creation expression (`CreateMessage` in grammar: `'.'? IDENTIFIER ('.' IDENTIFIER)* '{' ... '}'`). +// Unlike standalone identifiers or selector chains, reserved identifiers (e.g. `import{}` or `import.Foo{}`) +// are permitted in message type names. +func (p *prattParserWorker) isStructCreationAhead() bool { + if p.peekTok.kind == tokLeftBrace { + return true + } + if p.peekTok.kind != tokDot { + return false + } + savedPos := p.lexer.SavePosition() + defer p.lexer.RestorePosition(savedPos) + + tok := p.peekTok + for tok.kind == tokDot { + tok = p.scanNextSignificantToken() + if tok.kind != tokIdent && tok.kind != tokReservedWord { + return false + } + // Quoted identifiers are not allowed in struct creation expressions. + text := p.tokenText(tok) + if len(text) > 0 && text[0] == '`' { + return false + } + tok = p.scanNextSignificantToken() + } + return tok.kind == tokLeftBrace +} + +func (p *prattParserWorker) nextToken() token { + p.currTok = p.peekTok + if p.isRecoveryLimitExceeded() { + p.peekTok = token{kind: tokEnd, start: p.length, end: p.length} + return p.currTok + } + if p.peekTok.kind != tokEnd { + p.peekTok = p.nextSignificantToken() + } + return p.currTok +} + +func (p *prattParserWorker) tokenText(tok token) string { + if tok.start >= 0 && tok.end >= tok.start && tok.end <= p.length { + return p.content.Slice(int(tok.start), int(tok.end)) + } + return "" +} + +func (p *prattParserWorker) nextID(tok token) int64 { + return p.helper.idFromOffsets(tok.start, tok.end) +} + +func (p *prattParserWorker) expect(kind tokenKind, msg string) bool { + if p.peekTok.kind == kind { + p.nextToken() + return true + } + if p.recursionLimitExceeded || p.isRecoveryLimitExceeded() { + return false + } + if p.peekTok.kind != tokError { + if msg == "" { + tokText := p.tokenText(p.peekTok) + formattedTok := fmt.Sprintf("'%s'", tokText) + if p.peekTok.kind == tokEnd { + formattedTok = "" + } + msg = fmt.Sprintf("mismatched input %s expecting '%s'", formattedTok, kind.String()) + } + p.reportSyntaxError(p.peekTok, "%s", msg) + } + p.synchronizeOnDelimiter() + return false +} + +func (p *prattParserWorker) synchronizeOnDelimiter() { + if p.recursionLimitExceeded || p.isRecoveryLimitExceeded() { + p.peekTok = token{kind: tokEnd, start: p.length, end: p.length} + return + } + for p.peekTok.kind != tokEnd { + if p.peekTok.kind == tokComma || + p.peekTok.kind == tokRightParen || + p.peekTok.kind == tokRightBracket || + p.peekTok.kind == tokRightBrace { + break + } + p.nextToken() + } +} + +func (p *prattParserWorker) reportError(ctx any, format string, args ...any) ast.Expr { + if p.isRecoveryLimitExceeded() { + return p.helper.newExpr(common.NoLocation) + } + p.errorCount++ + var location common.Location + err := p.helper.newExpr(ctx) + switch c := ctx.(type) { + case common.Location: + location = c + case token: + location = p.helper.getLocation(err.ID()) + default: + location = p.helper.getLocation(err.ID()) + } + if p.errorCount <= p.errorReportingLimit { + p.errors.reportErrorAtID(err.ID(), location, format, args...) + } + if p.isRecoveryLimitExceeded() { + p.peekTok = token{kind: tokEnd, start: p.length, end: p.length} + } + return err +} + +func (p *prattParserWorker) reportSyntaxError(ctx any, format string, args ...any) ast.Expr { + return p.reportError(ctx, "Syntax error: "+format, args...) +} + +func (p *prattParserWorker) newLogicManager(function string, term ast.Expr) *logicManager { + if p.enableVariadicOperatorASTs { + return newVariadicLogicManager(p.exprFactory, function, term) + } + return newBalancingLogicManager(p.exprFactory, function, term) +} + +func (p *prattParserWorker) globalCallOrMacro(exprID int64, function string, args ...ast.Expr) ast.Expr { + if expr, found := p.expandMacro(exprID, function, nil, args...); found { + return expr + } + return p.helper.newGlobalCall(exprID, function, args...) +} + +func (p *prattParserWorker) receiverCallOrMacro(exprID int64, function string, target ast.Expr, args ...ast.Expr) ast.Expr { + if expr, found := p.expandMacro(exprID, function, target, args...); found { + return expr + } + return p.helper.newReceiverCall(exprID, function, target, args...) +} + +func (p *prattParserWorker) expandMacro(exprID int64, function string, target ast.Expr, args ...ast.Expr) (ast.Expr, bool) { + if len(p.macros) == 0 { + return nil, false + } + macro, found := p.macros[makeMacroKey(function, len(args), target != nil)] + if !found { + macro, found = p.macros[makeVarArgMacroKey(function, target != nil)] + if !found { + return nil, false + } + } + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } + eh := exprHelperPool.Get().(*exprHelper) + eh.parserHelper = p.helper + eh.id = exprID + expr, err := macro.Expander()(eh, target, args) + exprHelperPool.Put(eh) + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } + if err != nil { + loc := err.Location + if loc == nil { + loc = p.helper.getLocation(exprID) + } + p.helper.deleteID(exprID) + return p.reportError(loc, "%s", err.Message), true + } + if expr == nil { + return nil, false + } + if p.populateMacroCalls { + p.helper.addMacroCall(expr.ID(), function, target, args...) + } + p.helper.deleteID(exprID) + return expr, true +} + +func (p *prattParserWorker) normalizeIdent(tok token, allowQuoted bool, isQuoted *bool) string { + if isQuoted != nil { + *isQuoted = false + } + text := p.tokenText(tok) + if len(text) == 0 { + return "" + } + if text[0] == '`' { + if isQuoted != nil { + *isQuoted = true + } + if !allowQuoted { + p.reportError(tok, "unexpected quoted identifier") + return "" + } + if !p.enableIdentEscapeSyntax { + p.reportError(tok, "unsupported syntax: '`'") + } + if len(text) < 2 || text[len(text)-1] != '`' { + p.reportError(tok, "unterminated quoted identifier") + return "" + } + // Validate the quoted identifier syntax: + // ESC_IDENTIFIER : '`' (LETTER | DIGIT | '_' | '.' | '-' | '/' | ' ')+ '`'; + inner := text[1 : len(text)-1] + if len(inner) == 0 { + p.reportError(tok, "unexpected quoted identifier") + return "" + } + for _, c := range inner { + if !isAlpha(c) && !isDigit(c) && c != '_' && c != '.' && c != '-' && c != '/' && c != ' ' { + if c == '@' && p.enableCallEscapeSyntax { + continue + } + p.reportError(tok, "unexpected quoted identifier") + return "" + } + } + return inner + } + return text +} + +func (p *prattParserWorker) parse() ast.Expr { + expr := p.parseExpr() + if p.recursionLimitExceeded || p.isRecoveryLimitExceeded() { + return expr + } + if p.peekTok.kind != tokEnd && p.peekTok.kind != tokError { + p.reportSyntaxError(p.peekTok, "unexpected token after expression") + } + return expr +} + +func (p *prattParserWorker) parseExpr() ast.Expr { + if p.recursionLimitExceeded || p.isRecoveryLimitExceeded() { + return p.helper.newExpr(common.NoLocation) + } + if p.checkRecursion(p.recursionDepth + 1) { + return p.helper.newExpr(common.NoLocation) + } + p.recursionDepth++ + expr := p.parseBinaryAndTernary(0) + p.recursionDepth-- + return expr +} + +// parseElementExpr parses one element of a delimited construct (list, map, struct or argument +// list), accumulating lastParsedDepth to the deepest element seen so far. A construct is as +// deep as its deepest element, not its last one, so callers must reset lastParsedDepth to 0 +// before the first element. +func (p *prattParserWorker) parseElementExpr() ast.Expr { + maxDepth := p.lastParsedDepth + expr := p.parseExpr() + if maxDepth > p.lastParsedDepth { + p.lastParsedDepth = maxDepth + } + return expr +} + +func (p *prattParserWorker) parseBinaryAndTernary(minPrec int) ast.Expr { + lhs := p.parseSelectorChain() + return p.parseBinaryAndTernaryFromLhs(lhs, minPrec, p.lastParsedDepth) +} + +// parseBinaryAndTernaryFromLhs parses the operators following lhs, which is lhsDepth deep, and +// leaves the depth of the resulting expression in lastParsedDepth. +func (p *prattParserWorker) parseBinaryAndTernaryFromLhs(lhs ast.Expr, minPrec int, lhsDepth int) ast.Expr { + depth := lhsDepth + for !p.recursionLimitExceeded && !p.isRecoveryLimitExceeded() { + tok := p.peekTok.kind + if tok == tokQuestion && minPrec <= 0 { + lhs = p.parseTernary(lhs, depth) + depth = p.lastParsedDepth + continue + } + + opInfo := getBinaryOpInfo(tok) + if opInfo.kind == tokError || opInfo.precedence < minPrec { + break + } + + if opInfo.name == operators.LogicalOr || opInfo.name == operators.LogicalAnd { + lhs = p.parseLogicalChain(lhs, opInfo, depth) + depth = p.lastParsedDepth + continue + } + + opTok := p.nextToken() + opID := p.nextID(opTok) + rhs := p.parseBinaryAndTernary(opInfo.precedence + 1) + lhs = p.helper.newGlobalCall(opID, opInfo.name, lhs, rhs) + // lastParsedDepth is the depth of the rhs just parsed. The operator node + // sits one level above whichever operand is deeper: "x + a.b.c.d" reaches + // 4 through its rhs and "a.b.c.d + x" reaches 4 through its lhs. + depth = max(depth, p.lastParsedDepth) + 1 + p.checkRecursion(depth) + } + p.lastParsedDepth = depth + return lhs +} + +// parseTernary parses the branches of a conditional whose condition, lhs, is condDepth deep, +// and leaves the depth of the conditional in lastParsedDepth. +func (p *prattParserWorker) parseTernary(lhs ast.Expr, condDepth int) ast.Expr { + qTok := p.nextToken() + opID := p.nextID(qTok) + trueExpr := p.parseBinaryAndTernary(1) + depth := max(condDepth, p.lastParsedDepth) + if !p.expect(tokColon, "expected ':' in conditional expression") { + p.lastParsedDepth = condDepth + return lhs + } + falseExpr := p.parseExpr() + // The conditional node sits one level above the deepest of its operands. + depth = max(depth, p.lastParsedDepth) + 1 + p.lastParsedDepth = depth + p.checkRecursion(depth) + return p.helper.newGlobalCall(opID, operators.Conditional, lhs, trueExpr, falseExpr) +} + +// parseLogicalChain parses a run of '&&' or '||' operators following lhs, which is lhsDepth +// deep, and leaves the depth of the resulting expression in lastParsedDepth. Logical operators +// do not add to the depth: the chain is as deep as its deepest term. +func (p *prattParserWorker) parseLogicalChain(lhs ast.Expr, opInfo binaryOpInfo, lhsDepth int) ast.Expr { + depth := lhsDepth + l := p.newLogicManager(opInfo.name, lhs) + for !p.recursionLimitExceeded && !p.isRecoveryLimitExceeded() && p.peekTok.kind == opInfo.kind { + opTok := p.nextToken() + rhs := p.parseBinaryAndTernary(opInfo.precedence + 1) + depth = max(depth, p.lastParsedDepth) + opID := p.nextID(opTok) + l.addTerm(opID, rhs) + } + p.lastParsedDepth = depth + return l.toExpr() +} + +func (p *prattParserWorker) parseSelectorChain() ast.Expr { + p.lastParsedDepth = 0 + tok := p.peekTok.kind + if tok == tokExclamation || tok == tokMinus { + return p.parseUnaryOps() + } + return p.parseMember() +} + +func (p *prattParserWorker) parseMember() ast.Expr { + p.lastParsedDepth = 0 + canBeStructName := false + lhs := p.parsePrimary(&canBeStructName) + return p.parseSelectorChainTail(lhs, canBeStructName, p.lastParsedDepth) +} + +// parseSelectorChainTail parses the field selections, member calls, indexes and struct +// creations following lhs, which is lhsDepth deep, and leaves the depth of the resulting +// expression in lastParsedDepth. +func (p *prattParserWorker) parseSelectorChainTail(lhs ast.Expr, canBeStructName bool, lhsDepth int) ast.Expr { + depth := lhsDepth +loop: + for !p.recursionLimitExceeded { + switch p.peekTok.kind { + case tokDot: + dotTok := p.nextToken() + optional := false + if p.peekTok.kind == tokQuestion { + p.nextToken() + optional = true + if !p.enableOptionalSyntax { + p.reportError(dotTok, "unsupported syntax '.?'") + } + } + fieldTok := p.nextToken() + if fieldTok.kind != tokIdent && fieldTok.kind != tokReservedWord { + if fieldTok.kind != tokError { + p.reportSyntaxError(fieldTok, "expected identifier after '.'") + } + p.synchronizeOnDelimiter() + break loop + } + isMemberCall := p.peekTok.kind == tokLeftParen + var isQuoted bool + field := p.normalizeIdent(fieldTok, p.enableCallEscapeSyntax || !isMemberCall, &isQuoted) + depth++ + if optional { + fieldID := p.nextID(fieldTok) + opID := p.nextID(dotTok) + lhs = p.helper.newGlobalCall(opID, operators.OptSelect, lhs, p.helper.newLiteralString(fieldID, field)) + canBeStructName = false + } else if isMemberCall { + lparen := p.nextToken() + callID := p.nextID(lparen) + args := p.parseArguments(tokRightParen) + lhs = p.receiverCallOrMacro(callID, field, lhs, args...) + // parseArguments leaves lastParsedDepth at the deepest argument. + // Arguments hang one level below the call node, so "a.f(b.c.d.e)" is 4 + // deep. The max preserves the selectors already walked when the + // arguments are shallower, as in "a.b.c.f(1)". + depth = max(depth, p.lastParsedDepth+1) + canBeStructName = false + } else { + dotID := p.nextID(dotTok) + lhs = p.helper.newSelect(dotID, lhs, field) + canBeStructName = canBeStructName && !isQuoted + } + // A plain qualified name such as "a.b.Msg" may still turn out to be the + // type name of a struct creation, which adds no depth, so its field + // selections are only checked once the chain is known to be something else. + if !canBeStructName { + p.checkRecursion(depth) + } + case tokLeftBracket: + bracketTok := p.nextToken() + opID := p.nextID(bracketTok) + optional := false + if p.peekTok.kind == tokQuestion { + p.nextToken() + optional = true + if !p.enableOptionalSyntax { + p.reportError(bracketTok, "unsupported syntax '[?'") + } + } + index := p.parseExpr() + p.expect(tokRightBracket, "expected ']'") + opName := operators.Index + if optional { + opName = operators.OptIndex + } + lhs = p.helper.newGlobalCall(opID, opName, lhs, index) + // lastParsedDepth is the depth of the index expression just parsed. The + // index node sits one level above the deeper of its operand and its index, + // so "a[b.c.d.e]" is 4 deep and "a.b.c[0]" is 3 deep. + depth = max(depth, p.lastParsedDepth) + 1 + canBeStructName = false + p.checkRecursion(depth) + case tokLeftBrace: + if !canBeStructName { + break loop + } + structName, ok := p.extractStructName(lhs) + if !ok { + break loop + } + objID := p.nextID(p.peekTok) + lhs = p.parseStruct(objID, structName) + // parseStruct leaves lastParsedDepth at the deepest field value. Struct + // creation is a primary rather than a chain link and its qualified type + // name is part of it, so neither adds a level: "a.b.Msg{f: 1}" is 0 deep + // and "Msg{f: a.b.c.d}" is 3 deep. + depth = p.lastParsedDepth + canBeStructName = false + default: + break loop + } + } + // A chain that ended as a plain qualified name, such as "a.b.c", still needs + // to be checked. + if canBeStructName { + p.checkRecursion(depth) + } + p.lastParsedDepth = depth + return lhs +} + +// extractStructName converts a chain of pre-parsed AST nodes representing field selections, +// such as "a.b.Msg", into a qualified struct type name. +func (p *prattParserWorker) extractStructName(expr ast.Expr) (string, bool) { + var name string + for expr != nil && expr.Kind() == ast.SelectKind { + sel := expr.AsSelect() + if sel.IsTestOnly() { + return "", false + } + p.helper.deleteID(expr.ID()) + name = "." + sel.FieldName() + name + expr = sel.Operand() + } + if expr == nil || expr.Kind() != ast.IdentKind { + return "", false + } + p.helper.deleteID(expr.ID()) + return expr.AsIdent() + name, true +} + +func (p *prattParserWorker) parseStruct(objID int64, structName string) ast.Expr { + p.nextToken() // consumes { + var fields []ast.EntryExpr + p.lastParsedDepth = 0 + for p.peekTok.kind != tokRightBrace && p.peekTok.kind != tokEnd { + optional := false + if p.peekTok.kind == tokQuestion { + q := p.nextToken() + optional = true + if !p.enableOptionalSyntax { + p.reportError(q, "unsupported syntax '?'") + } + } + fieldTok := p.nextToken() + if fieldTok.kind != tokIdent && fieldTok.kind != tokReservedWord { + p.reportSyntaxError(fieldTok, "expected struct field name") + p.synchronizeOnDelimiter() + break + } + fieldName := p.normalizeIdent(fieldTok, true, nil) + colonTok := p.peekTok + if !p.expect(tokColon, "expected ':' in struct field") { + break + } + fieldID := p.nextID(colonTok) + val := p.parseElementExpr() + fields = append(fields, p.helper.newObjectField(fieldID, fieldName, val, optional)) + if p.peekTok.kind == tokComma { + p.nextToken() + } else { + break + } + } + p.expect(tokRightBrace, "expected '}'") + return p.helper.newObject(objID, structName, fields...) +} + +func (p *prattParserWorker) parseUnaryOps() ast.Expr { + firstOp := p.nextToken() + opType := firstOp.kind + ops := []token{firstOp} + for p.peekTok.kind == opType { + ops = append(ops, p.nextToken()) + } + + if opType == tokMinus && len(ops) == 1 && (p.peekTok.kind == tokInt || p.peekTok.kind == tokFloat) { + opID := p.nextID(firstOp) + var lhs ast.Expr + if p.peekTok.kind == tokInt { + lhs = p.parseNegativeIntLiteral(opID) + } else { + lhs = p.parseNegativeDoubleLiteral(opID) + } + tok := p.peekTok.kind + if tok == tokDot || tok == tokLeftBracket || tok == tokLeftBrace { + lhs = p.parseSelectorChainTail(lhs, false, 0) + } + return lhs + } + + if len(ops)%2 == 0 { + ops = ops[:0] + } else { + ops = ops[:1] + } + + type unaryOpWithID struct { + token token + id int64 + } + retainedOps := make([]unaryOpWithID, len(ops)) + for i, op := range ops { + retainedOps[i] = unaryOpWithID{token: op, id: p.nextID(op)} + } + + var operand ast.Expr + if opType == tokExclamation && p.peekTok.kind == tokMinus { + minusTok := p.nextToken() + minusID := p.nextID(minusTok) + if p.peekTok.kind == tokInt { + operand = p.parseNegativeIntLiteral(minusID) + operand = p.parseSelectorChainTail(operand, false, 0) + } else if p.peekTok.kind == tokFloat { + operand = p.parseNegativeDoubleLiteral(minusID) + operand = p.parseSelectorChainTail(operand, false, 0) + } else { + p.reportSyntaxError(minusTok, "unexpected '-'") + operand = p.parseMember() + } + } else { + operand = p.parseMember() + } + if p.recursionLimitExceeded { + return p.helper.newExpr(common.NoLocation) + } + + for i := len(retainedOps) - 1; i >= 0; i-- { + opName := operators.LogicalNot + if retainedOps[i].token.kind == tokMinus { + opName = operators.Negate + } + operand = p.globalCallOrMacro(retainedOps[i].id, opName, operand) + } + return operand +} + +func (p *prattParserWorker) parsePrimary(canBeStructName *bool) ast.Expr { + if canBeStructName != nil { + *canBeStructName = false + } + switch p.peekTok.kind { + case tokLeftParen: + if p.recursionLimitExceeded || p.isRecoveryLimitExceeded() { + return p.helper.newExpr(common.NoLocation) + } + // To avoid deep call-stack recursion on heavily nested parentheses (e.g. + // "((((a))))" or "((((a + 1) + 1) + 1))"), consume all consecutive + // leading '(' tokens upfront, parse the innermost expression once, and + // then iteratively unwind each enclosing '(' from innermost to outermost. + // After consuming each matching ')', if more enclosing '(' remain open + // and the next token is not another ')', continue parsing any trailing + // selectors or binary/ternary operators belonging to that enclosing + // parenthesized level using the already-parsed inner expression as the + // LHS. + openParens := 0 + for p.peekTok.kind == tokLeftParen { + openParens++ + p.nextToken() + } + // Every '(' nested inside the enclosing expression is a nesting level and + // costs one unit of recursion budget, so charge all of them here: the + // innermost of openParens parentheses sits at recursionDepth + openParens - 1. + // recursionDepth itself only advances by 1 because the parens are unwound + // iteratively and entering parseBinaryAndTernary(0) below adds just one + // stack frame. Parentheses do not add to the depth of the AST. + if p.checkRecursion(p.recursionDepth + openParens - 1) { + return p.helper.newExpr(common.NoLocation) + } + p.recursionDepth++ + expr := p.parseBinaryAndTernary(0) + depth := p.lastParsedDepth + for i := 0; i < openParens; i++ { + p.expect(tokRightParen, "expected ')'") + if i < openParens-1 && p.peekTok.kind != tokRightParen { + tok := p.peekTok.kind + if tok == tokDot || tok == tokLeftBracket || tok == tokLeftBrace { + expr = p.parseSelectorChainTail(expr, false, depth) + depth = p.lastParsedDepth + } + expr = p.parseBinaryAndTernaryFromLhs(expr, 0, depth) + depth = p.lastParsedDepth + } + } + p.recursionDepth-- + p.lastParsedDepth = depth + return expr + case tokNull: + return p.helper.exprFactory.NewLiteral(p.nextID(p.nextToken()), types.NullValue) + case tokTrue: + tok := p.nextToken() + return p.helper.newLiteralBool(p.nextID(tok), true) + case tokFalse: + tok := p.nextToken() + return p.helper.newLiteralBool(p.nextID(tok), false) + case tokInt: + return p.parseIntLiteral() + case tokUint: + return p.parseUintLiteral() + case tokFloat: + return p.parseDoubleLiteral() + case tokString: + return p.parseStringLiteral() + case tokBytes: + return p.parseBytesLiteral() + case tokLeftBracket: + return p.parseList() + case tokLeftBrace: + return p.parseMap() + case tokDot, tokIdent, tokReservedWord: + return p.parseIdentOrCall(canBeStructName) + default: + badTok := p.nextToken() + if badTok.kind != tokError { + if badTok.kind == tokEnd { + p.reportSyntaxError(badTok, "mismatched input '' expecting expression") + } else { + p.reportSyntaxError(badTok, "unexpected token") + } + } + return p.helper.newExpr(badTok) + } +} + +func (p *prattParserWorker) parseList() ast.Expr { + openTok := p.nextToken() + listID := p.nextID(openTok) + var elems []ast.Expr + var optionals []int32 + p.lastParsedDepth = 0 + for p.peekTok.kind != tokRightBracket && p.peekTok.kind != tokEnd { + optional := false + if p.peekTok.kind == tokQuestion { + q := p.nextToken() + optional = true + if !p.enableOptionalSyntax { + p.reportError(q, "unsupported syntax '?'") + } + } + if optional { + optionals = append(optionals, int32(len(elems))) + } + elem := p.parseElementExpr() + elems = append(elems, elem) + if p.peekTok.kind == tokComma { + p.nextToken() + if p.peekTok.kind == tokRightBracket { + break + } + continue + } + break + } + p.expect(tokRightBracket, "expected ']'") + return p.helper.newList(listID, elems, optionals...) +} + +func (p *prattParserWorker) parseMap() ast.Expr { + openTok := p.nextToken() + mapID := p.nextID(openTok) + var entries []ast.EntryExpr + p.lastParsedDepth = 0 + for p.peekTok.kind != tokRightBrace && p.peekTok.kind != tokEnd { + optional := false + if p.peekTok.kind == tokQuestion { + q := p.nextToken() + optional = true + if !p.enableOptionalSyntax { + p.reportError(q, "unsupported syntax '?'") + } + } + entryID := p.helper.allocID() + key := p.parseElementExpr() + colonTok := p.peekTok + if !p.expect(tokColon, "expected ':' in map entry") { + break + } + p.helper.setTokenLocation(entryID, colonTok) + val := p.parseElementExpr() + entries = append(entries, p.helper.newMapEntry(entryID, key, val, optional)) + if p.peekTok.kind == tokComma { + p.nextToken() + if p.peekTok.kind == tokRightBrace { + break + } + continue + } + break + } + p.expect(tokRightBrace, "expected '}'") + return p.helper.newMap(mapID, entries...) +} + +func (p *prattParserWorker) parseIdentOrCall(canBeStructName *bool) ast.Expr { + leadingDot := false + firstTok := p.peekTok + if p.peekTok.kind == tokDot { + p.nextToken() + leadingDot = true + } + idTok := p.nextToken() + if idTok.kind != tokIdent && idTok.kind != tokReservedWord { + if idTok.kind != tokError { + p.reportSyntaxError(idTok, "expected identifier") + } + if canBeStructName != nil { + *canBeStructName = false + } + return p.helper.newExpr(idTok) + } + var isQuoted bool + idText := p.normalizeIdent(idTok, p.enableCallEscapeSyntax, &isQuoted) + if idTok.kind == tokReservedWord { + if _, ok := reservedIds[idText]; ok && !p.isStructCreationAhead() { + p.reportError(idTok, "reserved identifier: %s", idText) + } + } + name := idText + if leadingDot { + name = "." + idText + } + if p.peekTok.kind == tokLeftParen { + lparen := p.nextToken() + callID := p.nextID(lparen) + args := p.parseArguments(tokRightParen) + if canBeStructName != nil { + *canBeStructName = false + } + return p.globalCallOrMacro(callID, name, args...) + } + if canBeStructName != nil { + *canBeStructName = !isQuoted + } + targetTok := idTok + if leadingDot { + targetTok = firstTok + } + id := p.nextID(targetTok) + return p.helper.newIdent(id, name) +} + +func (p *prattParserWorker) parseArguments(closeTok tokenKind) []ast.Expr { + var args []ast.Expr + p.lastParsedDepth = 0 + if p.peekTok.kind != closeTok && p.peekTok.kind != tokEnd { + for { + args = append(args, p.parseElementExpr()) + if p.peekTok.kind == tokComma { + p.nextToken() + if p.peekTok.kind == closeTok { + p.reportSyntaxError(p.peekTok, "unexpected token") + break + } + continue + } + break + } + } + p.expect(closeTok, "") + return args +} + +func (p *prattParserWorker) parseIntLiteral() ast.Expr { + tok := p.nextToken() + id := p.nextID(tok) + text := p.tokenText(tok) + base := 10 + if strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X") { + base = 16 + text = text[2:] + } + val, err := strconv.ParseInt(text, base, 64) + if err != nil { + return p.reportSyntaxError(tok, "invalid int literal") + } + return p.helper.newLiteralInt(id, val) +} + +func (p *prattParserWorker) parseNegativeIntLiteral(opID int64) ast.Expr { + tok := p.nextToken() + text := p.tokenText(tok) + base := 10 + if strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X") { + base = 16 + text = text[2:] + } + val, err := strconv.ParseInt("-"+text, base, 64) + if err != nil { + return p.reportSyntaxError(tok, "invalid int literal") + } + return p.helper.newLiteralInt(opID, val) +} + +func (p *prattParserWorker) parseUintLiteral() ast.Expr { + tok := p.nextToken() + id := p.nextID(tok) + text := p.tokenText(tok) + text = text[:len(text)-1] + base := 10 + if strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X") { + base = 16 + text = text[2:] + } + val, err := strconv.ParseUint(text, base, 64) + if err != nil { + return p.reportSyntaxError(tok, "invalid uint literal") + } + return p.helper.newLiteralUint(id, val) +} + +func (p *prattParserWorker) parseDoubleLiteral() ast.Expr { + tok := p.nextToken() + id := p.nextID(tok) + text := p.tokenText(tok) + val, err := strconv.ParseFloat(text, 64) + if err != nil { + return p.reportSyntaxError(tok, "invalid double literal") + } + return p.helper.newLiteralDouble(id, val) +} + +func (p *prattParserWorker) parseNegativeDoubleLiteral(opID int64) ast.Expr { + tok := p.nextToken() + text := p.tokenText(tok) + val, err := strconv.ParseFloat(text, 64) + if err != nil { + return p.reportSyntaxError(tok, "invalid double literal") + } + return p.helper.newLiteralDouble(opID, -val) +} + +func (p *prattParserWorker) parseStringLiteral() ast.Expr { + tok := p.nextToken() + id := p.nextID(tok) + text := p.tokenText(tok) + unescaped, err := unescape(text, false) + if err != nil { + return p.reportError(tok, "%s", err.Error()) + } + return p.helper.newLiteralString(id, unescaped) +} + +func (p *prattParserWorker) parseBytesLiteral() ast.Expr { + tok := p.nextToken() + id := p.nextID(tok) + text := p.tokenText(tok) + if strings.HasPrefix(text, "b") || strings.HasPrefix(text, "B") { + text = text[1:] + } else if strings.HasPrefix(text, "rb") || strings.HasPrefix(text, "RB") || strings.HasPrefix(text, "rB") || strings.HasPrefix(text, "Rb") { + text = "r" + text[2:] + } else if strings.HasPrefix(text, "br") || strings.HasPrefix(text, "BR") || strings.HasPrefix(text, "bR") || strings.HasPrefix(text, "Br") { + text = text[1:] + } + unescaped, err := unescape(text, true) + if err != nil { + return p.reportError(tok, "%s", err.Error()) + } + return p.helper.newLiteralBytes(id, []byte(unescaped)) +} diff --git a/vendor/github.com/google/cel-go/parser/unescape.go b/vendor/cel.dev/cel-go/parser/unescape.go similarity index 100% rename from vendor/github.com/google/cel-go/parser/unescape.go rename to vendor/cel.dev/cel-go/parser/unescape.go diff --git a/vendor/github.com/google/cel-go/parser/unparser.go b/vendor/cel.dev/cel-go/parser/unparser.go similarity index 87% rename from vendor/github.com/google/cel-go/parser/unparser.go rename to vendor/cel.dev/cel-go/parser/unparser.go index d503a450e9d..db96504bda9 100644 --- a/vendor/github.com/google/cel-go/parser/unparser.go +++ b/vendor/cel.dev/cel-go/parser/unparser.go @@ -17,14 +17,15 @@ package parser import ( "errors" "fmt" + "math" "regexp" "strconv" "strings" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" + "cel.dev/cel-go/common/ast" + "cel.dev/cel-go/common/operators" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" ) // Unparse takes an input expression and source position information and generates a human-readable @@ -69,7 +70,42 @@ func Unparse(expr ast.Expr, info *ast.SourceInfo, opts ...UnparserOption) (strin var identifierPartPattern *regexp.Regexp = regexp.MustCompile(`^[A-Za-z_][0-9A-Za-z_]*$`) -func maybeQuoteField(field string) string { +func (un *unparser) maybeQuoteIdent(name string) string { + if !un.options.escapeIdentifiers { + return name + } + if name == "" { + return "" + } + if strings.HasPrefix(name, ".") { + return "." + un.maybeQuoteIdent(name[1:]) + } + if strings.Contains(name, ".") { + parts := strings.Split(name, ".") + for i, part := range parts { + parts[i] = un.maybeQuoteIdent(part) + } + return strings.Join(parts, ".") + } + if !identifierPartPattern.MatchString(name) || isReservedIdent(name) { + return "`" + name + "`" + } + return name +} + +func (un *unparser) maybeQuoteFunction(fun string) string { + return un.maybeQuoteIdent(fun) +} + +func isReservedIdent(name string) bool { + _, ok := reservedIds[name] + return ok +} + +func (un *unparser) maybeQuoteField(field string) string { + if !un.options.escapeIdentifiers { + return field + } if !identifierPartPattern.MatchString(field) || field == "in" { return "`" + field + "`" } @@ -220,8 +256,10 @@ func (un *unparser) visitCallFunc(expr ast.Expr) error { return err } un.str.WriteString(".") + un.str.WriteString(un.maybeQuoteIdent(fun)) + } else { + un.str.WriteString(un.maybeQuoteFunction(fun)) } - un.str.WriteString(fun) un.str.WriteString("(") for i, arg := range args { err := un.visit(arg) @@ -270,7 +308,7 @@ func (un *unparser) visitCallUnary(expr ast.Expr) error { return fmt.Errorf("cannot unmangle operator: %s", fun) } un.str.WriteString(unmangled) - nested := isComplexOperator(args[0]) + nested := isComplexOperator(args[0]) || isAmbiguousUnaryOperand(args[0]) return un.visitMaybeNested(args[0], nested) } @@ -334,7 +372,7 @@ func (un *unparser) visitConst(expr ast.Expr) error { } func (un *unparser) visitIdent(expr ast.Expr) error { - un.str.WriteString(expr.AsIdent()) + un.str.WriteString(un.maybeQuoteIdent(expr.AsIdent())) return nil } @@ -386,7 +424,7 @@ func (un *unparser) visitSelectInternal(operand ast.Expr, testOnly bool, op stri return err } un.str.WriteString(op) - un.str.WriteString(maybeQuoteField(field)) + un.str.WriteString(un.maybeQuoteField(field)) if testOnly { un.str.WriteString(")") } @@ -396,7 +434,7 @@ func (un *unparser) visitSelectInternal(operand ast.Expr, testOnly bool, op stri func (un *unparser) visitStructMsg(expr ast.Expr) error { m := expr.AsStruct() fields := m.Fields() - un.str.WriteString(m.TypeName()) + un.str.WriteString(un.maybeQuoteFunction(m.TypeName())) un.str.WriteString("{") for i, f := range fields { field := f.AsStructField() @@ -404,7 +442,7 @@ func (un *unparser) visitStructMsg(expr ast.Expr) error { if field.IsOptional() { un.str.WriteString("?") } - un.str.WriteString(maybeQuoteField(f)) + un.str.WriteString(un.maybeQuoteField(f)) un.str.WriteString(": ") v := field.Value() err := un.visit(v) @@ -507,6 +545,30 @@ func isComplexOperator(expr ast.Expr) bool { return false } +// Indicates whether the operand of a unary operator must be wrapped in parentheses in order for +// the unparsed expression to parse back to an equivalent AST. +// +// The CEL grammar parses a run of leading '!' or '-' tokens as a single unary expression and drops +// the operators when their count is even, and it also treats a leading '-' as part of an int or +// double literal. Emitting such an operand without parentheses either changes the expression, e.g. +// `!(!a)` would unparse to `!!a` which parses back to `a`, or produces output which does not parse +// at all, e.g. `-(!a)` would unparse to `-!a`. +func isAmbiguousUnaryOperand(expr ast.Expr) bool { + switch expr.Kind() { + case ast.CallKind: + fun := expr.AsCall().FunctionName() + return fun == operators.LogicalNot || fun == operators.Negate + case ast.LiteralKind: + switch lit := expr.AsLiteral().(type) { + case types.Int: + return lit < 0 + case types.Double: + return math.Signbit(float64(lit)) + } + } + return false +} + // Indicates whether it is a complex operation compared to another. // expr is *not* considered complex if it is not a call expression or has // less than two arguments, or if it has a higher precedence than op. @@ -586,6 +648,16 @@ type unparserOption struct { wrapOnColumn int operatorsToWrapOn map[string]bool wrapAfterColumnLimit bool + escapeIdentifiers bool +} + +// EscapeIdentifiers enables backtick (`) escaping for non-standard and reserved identifiers, +// function names, and field names when unparsing. +func EscapeIdentifiers(escape bool) UnparserOption { + return func(opt *unparserOption) (*unparserOption, error) { + opt.escapeIdentifiers = escape + return opt, nil + } } // WrapOnColumn wraps the output expression when its string length exceeds a specified limit diff --git a/vendor/github.com/google/cel-go/cel/program.go b/vendor/github.com/google/cel-go/cel/program.go deleted file mode 100644 index 1fb65877740..00000000000 --- a/vendor/github.com/google/cel-go/cel/program.go +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2019 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 cel - -import ( - "context" - "errors" - "fmt" - - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/functions" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/interpreter" -) - -// Program is an evaluable view of an Ast. -type Program interface { - // Eval returns the result of an evaluation of the Ast and environment against the input vars. - // - // The vars value may either be an `Activation` or a `map[string]any`. - // - // If the `OptTrackState`, `OptTrackCost` or `OptExhaustiveEval` flags are used, the `details` response will - // be non-nil. Given this caveat on `details`, the return state from evaluation will be: - // - // * `val`, `details`, `nil` - Successful evaluation of a non-error result. - // * `val`, `details`, `err` - Successful evaluation to an error result. - // * `nil`, `details`, `err` - Unsuccessful evaluation. - // - // An unsuccessful evaluation is typically the result of a series of incompatible `EnvOption` - // or `ProgramOption` values used in the creation of the evaluation environment or executable - // program. - Eval(any) (ref.Val, *EvalDetails, error) - - // ContextEval evaluates the program with a set of input variables and a context object in order - // to support cancellation and timeouts. This method must be used in conjunction with the - // InterruptCheckFrequency() option for cancellation interrupts to be impact evaluation. - // - // The vars value may either be an `Activation` or `map[string]any`. - // - // The output contract for `ContextEval` is otherwise identical to the `Eval` method. - ContextEval(context.Context, any) (ref.Val, *EvalDetails, error) -} - -// Activation used to resolve identifiers by name and references by id. -// -// An Activation is the primary mechanism by which a caller supplies input into a CEL program. -type Activation = interpreter.Activation - -// NewActivation returns an activation based on a map-based binding where the map keys are -// expected to be qualified names used with ResolveName calls. -// -// The input `bindings` may either be of type `Activation` or `map[string]any`. -// -// Lazy bindings may be supplied within the map-based input in either of the following forms: -// - func() any -// - func() ref.Val -// -// The output of the lazy binding will overwrite the variable reference in the internal map. -// -// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using -// the types.Adapter configured in the environment. -func NewActivation(bindings any) (Activation, error) { - return interpreter.NewActivation(bindings) -} - -// PartialActivation extends the Activation interface with a set of unknown AttributePatterns. -type PartialActivation = interpreter.PartialActivation - -// NoVars returns an empty Activation. -func NoVars() Activation { - return interpreter.EmptyActivation() -} - -// PartialVars returns a PartialActivation which contains variables and a set of AttributePattern -// values that indicate variables or parts of variables whose value are not yet known. -// -// This method relies on manually configured sets of missing attribute patterns. For a method which -// infers the missing variables from the input and the configured environment, use Env.PartialVars(). -// -// The `vars` value may either be an Activation or any valid input to the NewActivation call. -func PartialVars(vars any, - unknowns ...*AttributePatternType) (PartialActivation, error) { - return interpreter.NewPartialActivation(vars, unknowns...) -} - -// AttributePattern returns an AttributePattern that matches a top-level variable. The pattern is -// mutable, and its methods support the specification of one or more qualifier patterns. -// -// For example, the AttributePattern(`a`).QualString(`b`) represents a variable access `a` with a -// string field or index qualification `b`. This pattern will match Attributes `a`, and `a.b`, -// but not `a.c`. -// -// When using a CEL expression within a container, e.g. a package or namespace, the variable name -// in the pattern must match the qualified name produced during the variable namespace resolution. -// For example, when variable `a` is declared within an expression whose container is `ns.app`, the -// fully qualified variable name may be `ns.app.a`, `ns.a`, or `a` per the CEL namespace resolution -// rules. Pick the fully qualified variable name that makes sense within the container as the -// AttributePattern `varName` argument. -func AttributePattern(varName string) *AttributePatternType { - return interpreter.NewAttributePattern(varName) -} - -// AttributePatternType represents a top-level variable with an optional set of qualifier patterns. -// -// See the interpreter.AttributePattern and interpreter.AttributeQualifierPattern for more info -// about how to create and manipulate AttributePattern values. -type AttributePatternType = interpreter.AttributePattern - -// EvalDetails holds additional information observed during the Eval() call. -type EvalDetails struct { - state interpreter.EvalState - costTracker *interpreter.CostTracker -} - -// State of the evaluation, non-nil if the OptTrackState or OptExhaustiveEval is specified -// within EvalOptions. -func (ed *EvalDetails) State() interpreter.EvalState { - if ed == nil { - return interpreter.NewEvalState() - } - return ed.state -} - -// ActualCost returns the tracked cost through the course of execution when `CostTracking` is enabled. -// Otherwise, returns nil if the cost was not enabled. -func (ed *EvalDetails) ActualCost() *uint64 { - if ed == nil || ed.costTracker == nil { - return nil - } - cost := ed.costTracker.ActualCost() - return &cost -} - -// prog is the internal implementation of the Program interface. -type prog struct { - *Env - evalOpts EvalOption - defaultVars Activation - dispatcher interpreter.Dispatcher - interpreter interpreter.Interpreter - interruptCheckFrequency uint - - // Intermediate state used to configure the InterpretableDecorator set provided - // to the initInterpretable call. - plannerOptions []interpreter.PlannerOption - regexOptimizations []*interpreter.RegexOptimization - - // Interpretable configured from an Ast and aggregate decorator set based on program options. - interpretable interpreter.InterpretableV2 - observable *interpreter.ObservableInterpretable - callCostEstimator interpreter.ActualCostEstimator - costOptions []interpreter.CostTrackerOption - costLimit *uint64 -} - -// newProgram creates a program instance with an environment, an ast, and an optional list of -// ProgramOption values. -// -// If the program cannot be configured the prog will be nil, with a non-nil error response. -func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { - // Build the dispatcher, interpreter, and default program value. - disp := interpreter.NewDispatcher() - - // Ensure the default attribute factory is set after the adapter and provider are - // configured. - p := &prog{ - Env: e, - plannerOptions: []interpreter.PlannerOption{}, - dispatcher: disp, - costOptions: []interpreter.CostTrackerOption{}, - } - - // Configure the program via the ProgramOption values. - var err error - for _, opt := range opts { - p, err = opt(p) - if err != nil { - return nil, err - } - } - - e.funcBindOnce.Do(func() { - var bindings []*functions.Overload - e.functionBindings = []*functions.Overload{} - for _, fn := range e.functions { - bindings, err = fn.Bindings() - if err != nil { - return - } - e.functionBindings = append(e.functionBindings, bindings...) - } - }) - if err != nil { - return nil, err - } - - // Add the function bindings created via Function() options. - err = disp.Add(e.functionBindings...) - if err != nil { - return nil, err - } - - // Set the attribute factory after the options have been set. - var attrFactory interpreter.AttributeFactory - attrFactorOpts := []interpreter.AttrFactoryOption{ - interpreter.EnableErrorOnBadPresenceTest(p.HasFeature(featureEnableErrorOnBadPresenceTest)), - } - if a.SourceInfo().HasExtension("json_name", ast.NewExtensionVersion(1, 1)) { - if !e.HasFeature(featureJSONFieldNames) { - return nil, errors.New("the AST extension 'json_name' requires the option cel.JSONFieldNames(true)") - } - } - // Configure the type provider, considering whether the AST indicates whether it supports JSON field names - if p.evalOpts&OptPartialEval == OptPartialEval { - attrFactory = interpreter.NewPartialAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...) - } else { - attrFactory = interpreter.NewAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...) - } - interp := interpreter.NewInterpreter(disp, e.Container, e.provider, e.adapter, attrFactory) - p.interpreter = interp - - // Translate the EvalOption flags into InterpretableDecorator instances. - plannerOptions := make([]interpreter.PlannerOption, len(p.plannerOptions)) - copy(plannerOptions, p.plannerOptions) - - // Enable interrupt checking if there's a non-zero check frequency - if p.interruptCheckFrequency > 0 { - plannerOptions = append(plannerOptions, interpreter.InterruptableEval()) - } - // Enable constant folding first. - if p.evalOpts&OptOptimize == OptOptimize { - plannerOptions = append(plannerOptions, interpreter.Optimize()) - p.regexOptimizations = append(p.regexOptimizations, interpreter.MatchesRegexOptimization) - } - // Enable regex compilation of constants immediately after folding constants. - if len(p.regexOptimizations) > 0 { - plannerOptions = append(plannerOptions, interpreter.CompileRegexConstants(p.regexOptimizations...)) - } - - // Enable exhaustive eval, state tracking and cost tracking last since they require a factory. - if p.evalOpts&(OptExhaustiveEval|OptTrackState|OptTrackCost) != 0 { - costOptCount := len(p.costOptions) - if p.costLimit != nil { - costOptCount++ - } - costOpts := make([]interpreter.CostTrackerOption, 0, costOptCount) - costOpts = append(costOpts, p.costOptions...) - 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 tracker.Clone() - } - var observers []interpreter.PlannerOption - if p.evalOpts&(OptExhaustiveEval|OptTrackState) != 0 { - // EvalStateObserver is required for OptExhaustiveEval. - observers = append(observers, interpreter.EvalStateObserver()) - } - if p.evalOpts&OptTrackCost == OptTrackCost { - observers = append(observers, interpreter.CostObserver(interpreter.CostTrackerFactory(trackerFactory))) - } - // Enable exhaustive eval over a basic observer since it offers a superset of features. - if p.evalOpts&OptExhaustiveEval == OptExhaustiveEval { - plannerOptions = append(plannerOptions, - append([]interpreter.PlannerOption{interpreter.ExhaustiveEval()}, observers...)...) - } else if len(observers) > 0 { - plannerOptions = append(plannerOptions, observers...) - } - } - return p.initInterpretable(a, plannerOptions) -} - -func (p *prog) initInterpretable(a *ast.AST, plannerOptions []interpreter.PlannerOption) (*prog, error) { - // When the AST has been exprAST it contains metadata that can be used to speed up program execution. - interpretable, err := p.interpreter.NewInterpretable(a, plannerOptions...) - if err != nil { - return nil, err - } - p.interpretable = interpretable - if oi, ok := interpretable.(*interpreter.ObservableInterpretable); ok { - p.observable = oi - } - return p, nil -} - -// Eval implements the Program interface method. -func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) { - // Configure error recovery for unexpected panics during evaluation. Note, the use of named - // return values makes it possible to modify the error response during the recovery - // function. - defer func() { - if r := recover(); r != nil { - switch t := r.(type) { - case interpreter.EvalCancelledError: - err = t - default: - err = fmt.Errorf("internal error: %v", r) - } - } - }() - // Build a hierarchical activation if there are default vars set. - 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.ObserveExec(frame, func(observed any) { - switch o := observed.(type) { - case interpreter.EvalState: - det.state = o - case *interpreter.CostTracker: - det.costTracker = o - } - }) - } else { - 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 - // RPC signature which allows for multiple errors to be returned, but should be sufficient. - if types.IsError(out) { - err = out.(*types.Err) - } - return -} - -// ContextEval implements the Program interface. -func (p *prog) ContextEval(ctx context.Context, input any) (ref.Val, *EvalDetails, error) { - if ctx == nil { - return nil, nil, fmt.Errorf("context can not be nil") - } - frame, err := p.newExecutionFrame(input) - if err != nil { - return nil, nil, err - } - 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, errEval -} - -// 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 - } - if p.defaultVars != nil { - // Update the frame's activation in place. - frame.Activation = interpreter.NewHierarchicalActivation(p.defaultVars, frame.Activation) - } - - return frame, nil -} diff --git a/vendor/github.com/google/cel-go/checker/cost.go b/vendor/github.com/google/cel-go/checker/cost.go deleted file mode 100644 index 3d7dd7ec4a2..00000000000 --- a/vendor/github.com/google/cel-go/checker/cost.go +++ /dev/null @@ -1,1055 +0,0 @@ -// Copyright 2022 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package checker - -import ( - "math" - - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/parser" -) - -// WARNING: Any changes to cost calculations in this file require a corresponding change in interpreter/runtimecost.go - -// CostEstimator estimates the sizes of variable length input data and the costs of functions. -type CostEstimator interface { - // EstimateSize returns a SizeEstimate for the given AstNode, or nil if the estimator has no - // estimate to provide. - // - // The size is equivalent to the result of the CEL `size()` function: - // * Number of unicode characters in a string - // * Number of bytes in a sequence - // * Number of map entries or number of list items. - // - // EstimateSize is only called for AstNodes where CEL does not know the size; EstimateSize is not - // called for values defined inline in CEL where the size is already obvious to CEL. - EstimateSize(element AstNode) *SizeEstimate - - // EstimateCallCost returns the estimated cost of an invocation, or nil if the estimator has no - // estimate to provide. - EstimateCallCost(function, overloadID string, target *AstNode, args []AstNode) *CallEstimate -} - -// CallEstimate includes a CostEstimate for the call, and an optional estimate of the result object size. -// The ResultSize should only be provided if the call results in a map, list, string or bytes. -type CallEstimate struct { - CostEstimate - - ResultSize *SizeEstimate -} - -// AstNode represents an AST node for the purpose of cost estimations. -type AstNode interface { - // Path returns a field path through the provided type declarations to the type of the AstNode, or nil if the AstNode does not - // represent type directly reachable from the provided type declarations. - // The first path element is a variable. All subsequent path elements are one of: field name, '@items', '@keys', '@values'. - Path() []string - - // Type returns the deduced type of the AstNode. - Type() *types.Type - - // Expr returns the expression of the AstNode. - Expr() ast.Expr - - // ComputedSize returns a size estimate of the AstNode derived from information available in the CEL expression. - // For constants and inline list and map declarations, the exact size is returned. For concatenated list, strings - // and bytes, the size is derived from the size estimates of the operands. nil is returned if there is no - // computed size available. - ComputedSize() *SizeEstimate -} - -type astNode struct { - path []string - t *types.Type - expr ast.Expr - derivedSize *SizeEstimate -} - -func (e astNode) Path() []string { - return e.path -} - -func (e astNode) Type() *types.Type { - return e.t -} - -func (e astNode) Expr() ast.Expr { - return e.expr -} - -func (e astNode) ComputedSize() *SizeEstimate { - return e.derivedSize -} - -// SizeEstimate represents an estimated size of a variable length string, bytes, map or list. -type SizeEstimate struct { - Min, Max uint64 -} - -// UnknownSizeEstimate returns a size between 0 and max uint -func UnknownSizeEstimate() SizeEstimate { - return unknownSizeEstimate -} - -// FixedSizeEstimate returns a size estimate with a fixed min and max range. -func FixedSizeEstimate(size uint64) SizeEstimate { - return SizeEstimate{Min: size, Max: size} -} - -// Add adds to another SizeEstimate and returns the sum. -// If add would result in an uint64 overflow, the result is math.MaxUint64. -func (se SizeEstimate) Add(sizeEstimate SizeEstimate) SizeEstimate { - return SizeEstimate{ - addUint64NoOverflow(se.Min, sizeEstimate.Min), - addUint64NoOverflow(se.Max, sizeEstimate.Max), - } -} - -// Multiply multiplies by another SizeEstimate and returns the product. -// If multiply would result in an uint64 overflow, the result is math.MaxUint64. -func (se SizeEstimate) Multiply(sizeEstimate SizeEstimate) SizeEstimate { - return SizeEstimate{ - multiplyUint64NoOverflow(se.Min, sizeEstimate.Min), - multiplyUint64NoOverflow(se.Max, sizeEstimate.Max), - } -} - -// MultiplyByCostFactor multiplies a SizeEstimate by a cost factor and returns the CostEstimate with the -// nearest integer of the result, rounded up. -func (se SizeEstimate) MultiplyByCostFactor(costPerUnit float64) CostEstimate { - return CostEstimate{ - multiplyByCostFactor(se.Min, costPerUnit), - multiplyByCostFactor(se.Max, costPerUnit), - } -} - -// MultiplyByCost multiplies by the cost and returns the product. -// If multiply would result in an uint64 overflow, the result is math.MaxUint64. -func (se SizeEstimate) MultiplyByCost(cost CostEstimate) CostEstimate { - return CostEstimate{ - multiplyUint64NoOverflow(se.Min, cost.Min), - multiplyUint64NoOverflow(se.Max, cost.Max), - } -} - -// Union returns a SizeEstimate that encompasses both input the SizeEstimate. -func (se SizeEstimate) Union(size SizeEstimate) SizeEstimate { - result := se - if size.Min < result.Min { - result.Min = size.Min - } - if size.Max > result.Max { - result.Max = size.Max - } - return result -} - -// AsCost converts a size estimates to an equivalent cost estimate. -func (se SizeEstimate) AsCost() CostEstimate { - return se.MultiplyByCostFactor(1) -} - -// CostEstimate represents an estimated cost range and provides add and multiply operations -// that do not overflow. -type CostEstimate struct { - Min, Max uint64 -} - -// UnknownCostEstimate returns a cost with an unknown impact. -func UnknownCostEstimate() CostEstimate { - return unknownCostEstimate -} - -// FixedCostEstimate returns a cost with a fixed min and max range. -func FixedCostEstimate(cost uint64) CostEstimate { - return CostEstimate{Min: cost, Max: cost} -} - -// Add adds the costs and returns the sum. -// If add would result in an uint64 overflow for the min or max, the value is set to math.MaxUint64. -func (ce CostEstimate) Add(cost CostEstimate) CostEstimate { - return CostEstimate{ - Min: addUint64NoOverflow(ce.Min, cost.Min), - Max: addUint64NoOverflow(ce.Max, cost.Max), - } -} - -// Multiply multiplies by the cost and returns the product. -// If multiply would result in an uint64 overflow, the result is math.MaxUint64. -func (ce CostEstimate) Multiply(cost CostEstimate) CostEstimate { - return CostEstimate{ - Min: multiplyUint64NoOverflow(ce.Min, cost.Min), - Max: multiplyUint64NoOverflow(ce.Max, cost.Max), - } -} - -// MultiplyByCostFactor multiplies a CostEstimate by a cost factor and returns the CostEstimate with the -// nearest integer of the result, rounded up. -func (ce CostEstimate) MultiplyByCostFactor(costPerUnit float64) CostEstimate { - return CostEstimate{ - Min: multiplyByCostFactor(ce.Min, costPerUnit), - Max: multiplyByCostFactor(ce.Max, costPerUnit), - } -} - -// Union returns a CostEstimate that encompasses both input the CostEstimates. -func (ce CostEstimate) Union(size CostEstimate) CostEstimate { - result := ce - if size.Min < result.Min { - result.Min = size.Min - } - if size.Max > result.Max { - result.Max = size.Max - } - return result -} - -// addUint64NoOverflow adds non-negative ints. If the result is exceeds math.MaxUint64, math.MaxUint64 -// is returned. -func addUint64NoOverflow(x, y uint64) uint64 { - if y > 0 && x > math.MaxUint64-y { - return math.MaxUint64 - } - return x + y -} - -// multiplyUint64NoOverflow multiplies non-negative ints. If the result is exceeds math.MaxUint64, math.MaxUint64 -// is returned. -func multiplyUint64NoOverflow(x, y uint64) uint64 { - if y != 0 && x > math.MaxUint64/y { - return math.MaxUint64 - } - return x * y -} - -// multiplyByFactor multiplies an integer by a cost factor float and returns the nearest integer value, rounded up. -func multiplyByCostFactor(x uint64, y float64) uint64 { - xFloat := float64(x) - if xFloat > 0 && y > 0 && xFloat > math.MaxUint64/y { - return math.MaxUint64 - } - ceil := math.Ceil(xFloat * y) - if ceil >= doubleTwoTo64 { - return math.MaxUint64 - } - return uint64(ceil) -} - -// CostOption configures flags which affect cost computations. -type CostOption func(*coster) error - -// PresenceTestHasCost determines whether presence testing has a cost of one or zero. -// -// Defaults to presence test has a cost of one. -func PresenceTestHasCost(hasCost bool) CostOption { - return func(c *coster) error { - if hasCost { - c.presenceTestCost = selectAndIdentCost - return nil - } - c.presenceTestCost = FixedCostEstimate(0) - return nil - } -} - -// FunctionEstimator provides a CallEstimate given the target and arguments for a specific function, overload pair. -type FunctionEstimator func(estimator CostEstimator, target *AstNode, args []AstNode) *CallEstimate - -// OverloadCostEstimate binds a FunctionCoster to a specific function overload ID. -// -// When a OverloadCostEstimate is provided, it will override the cost calculation of the CostEstimator provided to -// the Cost() call. -func OverloadCostEstimate(overloadID string, functionCoster FunctionEstimator) CostOption { - return func(c *coster) error { - c.overloadEstimators[overloadID] = functionCoster - return nil - } -} - -// Cost estimates the cost of the parsed and type checked CEL expression. -func Cost(checked *ast.AST, estimator CostEstimator, opts ...CostOption) (CostEstimate, error) { - c := &coster{ - checkedAST: checked, - estimator: estimator, - overloadEstimators: map[string]FunctionEstimator{}, - exprPaths: map[int64][]string{}, - localVars: make(scopes), - computedSizes: map[int64]SizeEstimate{}, - computedEntrySizes: map[int64]entrySizeEstimate{}, - presenceTestCost: FixedCostEstimate(1), - } - for _, opt := range opts { - err := opt(c) - if err != nil { - return CostEstimate{}, err - } - } - return c.cost(checked.Expr()), nil -} - -type coster struct { - // exprPaths maps from Expr Id to field path. - exprPaths map[int64][]string - // localVars tracks the local and iteration variables assigned during evaluation. - localVars scopes - // computedSizes tracks the computed sizes of call results. - computedSizes map[int64]SizeEstimate - // computedEntrySizes tracks the size of list and map entries - computedEntrySizes map[int64]entrySizeEstimate - - checkedAST *ast.AST - estimator CostEstimator - overloadEstimators map[string]FunctionEstimator - // presenceTestCost will either be a zero or one based on whether has() macros count against cost computations. - presenceTestCost CostEstimate -} - -// entrySizeEstimate captures the container kind and associated key/index and value SizeEstimate values. -// -// An entrySizeEstimate only exists if both the key/index and the value have SizeEstimate values, otherwise -// a nil entrySizeEstimate should be used. -type entrySizeEstimate struct { - containerKind types.Kind - key SizeEstimate - val SizeEstimate -} - -// container returns the container kind (list or map) of the entry. -func (s *entrySizeEstimate) container() types.Kind { - if s == nil { - return types.UnknownKind - } - return s.containerKind -} - -// keySize returns the SizeEstimate for the key if one exists. -func (s *entrySizeEstimate) keySize() *SizeEstimate { - if s == nil { - return nil - } - return &s.key -} - -// valSize returns the SizeEstimate for the value if one exists. -func (s *entrySizeEstimate) valSize() *SizeEstimate { - if s == nil { - return nil - } - return &s.val -} - -func (s *entrySizeEstimate) union(other *entrySizeEstimate) *entrySizeEstimate { - if s == nil || other == nil { - return nil - } - sk := s.key.Union(other.key) - sv := s.val.Union(other.val) - return &entrySizeEstimate{ - containerKind: s.containerKind, - key: sk, - val: sv, - } -} - -// localVar captures the local variable size and entrySize estimates if they exist for variables -type localVar struct { - exprID int64 - path []string - size *SizeEstimate - entrySize *entrySizeEstimate -} - -// scopes is a stack of variable name to integer id stack to handle scopes created by cel.bind() like macros -type scopes map[string][]*localVar - -func (s scopes) push(varName string, expr ast.Expr, path []string, size *SizeEstimate, entrySize *entrySizeEstimate) { - s[varName] = append(s[varName], &localVar{ - exprID: expr.ID(), - path: path, - size: size, - entrySize: entrySize, - }) -} - -func (s scopes) pop(varName string) { - varStack := s[varName] - s[varName] = varStack[:len(varStack)-1] -} - -func (s scopes) peek(varName string) (*localVar, bool) { - varStack := s[varName] - if len(varStack) > 0 { - return varStack[len(varStack)-1], true - } - return nil, false -} - -func (c *coster) pushIterKey(varName string, rangeExpr ast.Expr) { - entrySize := c.computeEntrySize(rangeExpr) - size := entrySize.keySize() - path := c.getPath(rangeExpr) - container := entrySize.container() - if container == types.UnknownKind { - container = c.getType(rangeExpr).Kind() - } - subpath := "@keys" - if container == types.ListKind { - subpath = "@indices" - } - c.localVars.push(varName, rangeExpr, append(path, subpath), size, nil) -} - -func (c *coster) pushIterValue(varName string, rangeExpr ast.Expr) { - entrySize := c.computeEntrySize(rangeExpr) - size := entrySize.valSize() - path := c.getPath(rangeExpr) - container := entrySize.container() - if container == types.UnknownKind { - container = c.getType(rangeExpr).Kind() - } - subpath := "@values" - if container == types.ListKind { - subpath = "@items" - } - c.localVars.push(varName, rangeExpr, append(path, subpath), size, nil) -} - -func (c *coster) pushIterSingle(varName string, rangeExpr ast.Expr) { - entrySize := c.computeEntrySize(rangeExpr) - size := entrySize.keySize() - subpath := "@keys" - container := entrySize.container() - if container == types.UnknownKind { - container = c.getType(rangeExpr).Kind() - } - if container == types.ListKind { - size = entrySize.valSize() - subpath = "@items" - } - path := c.getPath(rangeExpr) - c.localVars.push(varName, rangeExpr, append(path, subpath), size, nil) -} - -func (c *coster) pushLocalVar(varName string, e ast.Expr) { - path := c.getPath(e) - // note: retrieve the entry size for the local variable based on the size of the binding expression - // since the binding expression could be a list or map, the entry size should also be propagated - entrySize := c.computeEntrySize(e) - c.localVars.push(varName, e, path, c.computeSize(e), entrySize) -} - -func (c *coster) peekLocalVar(varName string) (*localVar, bool) { - return c.localVars.peek(varName) -} - -func (c *coster) popLocalVar(varName string) { - c.localVars.pop(varName) -} - -func (c *coster) cost(e ast.Expr) CostEstimate { - if e == nil { - return CostEstimate{} - } - var cost CostEstimate - switch e.Kind() { - case ast.LiteralKind: - cost = constCost - case ast.IdentKind: - cost = c.costIdent(e) - case ast.SelectKind: - cost = c.costSelect(e) - case ast.CallKind: - cost = c.costCall(e) - case ast.ListKind: - cost = c.costCreateList(e) - case ast.MapKind: - cost = c.costCreateMap(e) - case ast.StructKind: - cost = c.costCreateStruct(e) - case ast.ComprehensionKind: - if c.isBind(e) { - cost = c.costBind(e) - } else { - cost = c.costComprehension(e) - } - default: - return CostEstimate{} - } - return cost -} - -func (c *coster) costIdent(e ast.Expr) CostEstimate { - identName := e.AsIdent() - // build and track the field path - if v, ok := c.peekLocalVar(identName); ok { - c.addPath(e, v.path) - } else { - c.addPath(e, []string{identName}) - } - return selectAndIdentCost -} - -func (c *coster) costSelect(e ast.Expr) CostEstimate { - sel := e.AsSelect() - var sum CostEstimate - if sel.IsTestOnly() { - // recurse, but do not add any cost - // this is equivalent to how evalTestOnly increments the runtime cost counter - // but does not add any additional cost for the qualifier, except here we do - // the reverse (ident adds cost) - sum = sum.Add(c.presenceTestCost) - sum = sum.Add(c.cost(sel.Operand())) - return sum - } - sum = sum.Add(c.cost(sel.Operand())) - targetType := c.getType(sel.Operand()) - switch targetType.Kind() { - case types.MapKind, types.StructKind, types.TypeParamKind: - sum = sum.Add(selectAndIdentCost) - } - - // build and track the field path - c.addPath(e, append(c.getPath(sel.Operand()), sel.FieldName())) - return sum -} - -func (c *coster) costCall(e ast.Expr) CostEstimate { - // Dyn is just a way to disable type-checking, so return the cost of 1 with the cost of the argument - if dynEstimate := c.maybeUnwrapDynCall(e); dynEstimate != nil { - return *dynEstimate - } - - // Continue estimating the cost of all other calls. - call := e.AsCall() - args := call.Args() - var sum CostEstimate - - argTypes := make([]AstNode, len(args)) - argCosts := make([]CostEstimate, len(args)) - for i, arg := range args { - argCosts[i] = c.cost(arg) - argTypes[i] = c.newAstNode(arg) - } - - overloadIDs := c.checkedAST.GetOverloadIDs(e.ID()) - if len(overloadIDs) == 0 { - return CostEstimate{} - } - var targetType *AstNode - if call.IsMemberFunction() { - sum = sum.Add(c.cost(call.Target())) - var t AstNode = c.newAstNode(call.Target()) - targetType = &t - } - // Pick a cost estimate range that covers all the overload cost estimation ranges - fnCost := CostEstimate{Min: uint64(math.MaxUint64), Max: 0} - var resultSize *SizeEstimate - for _, overload := range overloadIDs { - overloadCost := c.functionCost(e, call.FunctionName(), overload, targetType, argTypes, argCosts) - fnCost = fnCost.Union(overloadCost.CostEstimate) - if overloadCost.ResultSize != nil { - if resultSize == nil { - resultSize = overloadCost.ResultSize - } else { - size := resultSize.Union(*overloadCost.ResultSize) - resultSize = &size - } - } - // build and track the field path for index operations - switch overload { - case overloads.IndexList: - if len(args) > 0 { - // note: assigning resultSize here could be redundant with the path-based lookup later - resultSize = c.computeEntrySize(args[0]).valSize() - c.addPath(e, append(c.getPath(args[0]), "@items")) - } - case overloads.IndexMap: - if len(args) > 0 { - resultSize = c.computeEntrySize(args[0]).valSize() - c.addPath(e, append(c.getPath(args[0]), "@values")) - } - } - if resultSize == nil { - resultSize = c.computeSize(e) - } - } - c.setSize(e, resultSize) - return sum.Add(fnCost) -} - -func (c *coster) maybeUnwrapDynCall(e ast.Expr) *CostEstimate { - call := e.AsCall() - if call.FunctionName() != "dyn" { - return nil - } - arg := call.Args()[0] - argCost := c.cost(arg) - c.copySizeEstimates(e, arg) - callCost := FixedCostEstimate(1).Add(argCost) - return &callCost -} - -func (c *coster) costCreateList(e ast.Expr) CostEstimate { - create := e.AsList() - var sum CostEstimate - itemSize := SizeEstimate{Min: math.MaxUint64, Max: 0} - if create.Size() == 0 { - itemSize.Min = 0 - } - for _, e := range create.Elements() { - sum = sum.Add(c.cost(e)) - is := c.sizeOrUnknown(e) - itemSize = itemSize.Union(is) - } - c.setEntrySize(e, &entrySizeEstimate{containerKind: types.ListKind, key: FixedSizeEstimate(1), val: itemSize}) - return sum.Add(createListBaseCost) -} - -func (c *coster) costCreateMap(e ast.Expr) CostEstimate { - mapVal := e.AsMap() - var sum CostEstimate - keySize := SizeEstimate{Min: math.MaxUint64, Max: 0} - valSize := SizeEstimate{Min: math.MaxUint64, Max: 0} - if mapVal.Size() == 0 { - valSize.Min = 0 - keySize.Min = 0 - } - for _, ent := range mapVal.Entries() { - entry := ent.AsMapEntry() - sum = sum.Add(c.cost(entry.Key())) - sum = sum.Add(c.cost(entry.Value())) - // Compute the key size range - ks := c.sizeOrUnknown(entry.Key()) - keySize = keySize.Union(ks) - // Compute the value size range - vs := c.sizeOrUnknown(entry.Value()) - valSize = valSize.Union(vs) - } - c.setEntrySize(e, &entrySizeEstimate{containerKind: types.MapKind, key: keySize, val: valSize}) - return sum.Add(createMapBaseCost) -} - -func (c *coster) costCreateStruct(e ast.Expr) CostEstimate { - msgVal := e.AsStruct() - var sum CostEstimate - for _, ent := range msgVal.Fields() { - field := ent.AsStructField() - sum = sum.Add(c.cost(field.Value())) - } - return sum.Add(createMessageBaseCost) -} - -func (c *coster) costComprehension(e ast.Expr) CostEstimate { - comp := e.AsComprehension() - var sum CostEstimate - sum = sum.Add(c.cost(comp.IterRange())) - sum = sum.Add(c.cost(comp.AccuInit())) - c.pushLocalVar(comp.AccuVar(), comp.AccuInit()) - - // Track the iterRange of each IterVar and AccuVar for field path construction - if comp.HasIterVar2() { - c.pushIterKey(comp.IterVar(), comp.IterRange()) - c.pushIterValue(comp.IterVar2(), comp.IterRange()) - } else { - c.pushIterSingle(comp.IterVar(), comp.IterRange()) - } - - // Determine the cost for each element in the loop - loopCost := c.cost(comp.LoopCondition()) - stepCost := c.cost(comp.LoopStep()) - - // Clear the intermediate variable tracking. - c.popLocalVar(comp.IterVar()) - if comp.HasIterVar2() { - c.popLocalVar(comp.IterVar2()) - } - - // Determine the result cost. - sum = sum.Add(c.cost(comp.Result())) - c.localVars.pop(comp.AccuVar()) - - // Estimate the cost of the loop. - rangeCnt := c.sizeOrUnknown(comp.IterRange()) - rangeCost := rangeCnt.MultiplyByCost(stepCost.Add(loopCost)) - sum = sum.Add(rangeCost) - - switch k := comp.AccuInit().Kind(); k { - case ast.LiteralKind: - c.setSize(e, c.computeSize(comp.AccuInit())) - case ast.ListKind, ast.MapKind: - c.setSize(e, &rangeCnt) - // For a step which produces a container value, it will have an entry size associated - // with its expression id. - if stepEntrySize := c.computeEntrySize(comp.LoopStep()); stepEntrySize != nil { - c.setEntrySize(e, stepEntrySize) - break - } - } - return sum -} - -func (c *coster) isBind(e ast.Expr) bool { - comp := e.AsComprehension() - iterRange := comp.IterRange() - loopCond := comp.LoopCondition() - return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 && - loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral() == types.False && - comp.AccuVar() != parser.AccumulatorName -} - -func (c *coster) costBind(e ast.Expr) CostEstimate { - comp := e.AsComprehension() - var sum CostEstimate - // Binds are lazily initialized, so we retain the cost of an empty iteration range. - sum = sum.Add(c.cost(comp.IterRange())) - sum = sum.Add(c.cost(comp.AccuInit())) - - c.pushLocalVar(comp.AccuVar(), comp.AccuInit()) - sum = sum.Add(c.cost(comp.Result())) - c.popLocalVar(comp.AccuVar()) - - // Associate the bind output size with the result size. - c.copySizeEstimates(e, comp.Result()) - return sum -} - -func (c *coster) functionCost(e ast.Expr, function, overloadID string, target *AstNode, args []AstNode, argCosts []CostEstimate) CallEstimate { - argCostSum := func() CostEstimate { - var sum CostEstimate - for _, a := range argCosts { - sum = sum.Add(a) - } - return sum - } - if len(c.overloadEstimators) != 0 { - if estimator, found := c.overloadEstimators[overloadID]; found { - if est := estimator(c.estimator, target, args); est != nil { - callEst := *est - return CallEstimate{CostEstimate: callEst.Add(argCostSum()), ResultSize: est.ResultSize} - } - } - } - if est := c.estimator.EstimateCallCost(function, overloadID, target, args); est != nil { - callEst := *est - return CallEstimate{CostEstimate: callEst.Add(argCostSum()), ResultSize: est.ResultSize} - } - switch overloadID { - // O(n) functions - case overloads.ExtFormatString: - if target != nil { - // ResultSize not calculated because we can't bound the max size. - return CallEstimate{ - CostEstimate: c.sizeOrUnknown(*target).MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum())} - } - case overloads.StringToBytes: - if len(args) == 1 { - sz := c.sizeOrUnknown(args[0]) - // ResultSize max is when each char converts to 4 bytes. - return CallEstimate{ - CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum()), - ResultSize: &SizeEstimate{Min: sz.Min, Max: sz.Max * 4}} - } - case overloads.BytesToString: - if len(args) == 1 { - sz := c.sizeOrUnknown(args[0]) - // ResultSize min is when 4 bytes convert to 1 char. - return CallEstimate{ - CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum()), - ResultSize: &SizeEstimate{Min: sz.Min / 4, Max: sz.Max}} - } - case overloads.ExtQuoteString: - if len(args) == 1 { - sz := c.sizeOrUnknown(args[0]) - // ResultSize max is when each char is escaped. 2 quote chars always added. - return CallEstimate{ - CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum()), - ResultSize: &SizeEstimate{Min: sz.Min + 2, Max: sz.Max*2 + 2}} - } - case overloads.StartsWithString, overloads.EndsWithString: - if len(args) == 1 { - return CallEstimate{CostEstimate: c.sizeOrUnknown(args[0]).MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum())} - } - case overloads.InList: - // If a list is composed entirely of constant values this is O(1), but we don't account for that here. - // We just assume all list containment checks are O(n). - if len(args) == 2 { - return CallEstimate{CostEstimate: c.sizeOrUnknown(args[1]).MultiplyByCostFactor(1).Add(argCostSum())} - } - // O(nm) functions - case overloads.Matches, overloads.MatchesString: - // https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL - 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(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(regexNode).MultiplyByCostFactor(common.RegexStringLengthCostFactor) - return CallEstimate{CostEstimate: strCost.Multiply(regexCost).Add(argCostSum())} - } - case overloads.ContainsString: - if target != nil && len(args) == 1 { - strCost := c.sizeOrUnknown(*target).MultiplyByCostFactor(common.StringTraversalCostFactor) - substrCost := c.sizeOrUnknown(args[0]).MultiplyByCostFactor(common.StringTraversalCostFactor) - return CallEstimate{CostEstimate: strCost.Multiply(substrCost).Add(argCostSum())} - } - case overloads.LogicalOr, overloads.LogicalAnd: - lhs := argCosts[0] - rhs := argCosts[1] - // min cost is min of LHS for short circuited && or || - argCost := CostEstimate{Min: lhs.Min, Max: lhs.Add(rhs).Max} - return CallEstimate{CostEstimate: argCost} - case overloads.Conditional: - size := c.sizeOrUnknown(args[1]).Union(c.sizeOrUnknown(args[2])) - resultEntrySize := c.computeEntrySize(args[1].Expr()).union(c.computeEntrySize(args[2].Expr())) - c.setEntrySize(e, resultEntrySize) - conditionalCost := argCosts[0] - ifTrueCost := argCosts[1] - ifFalseCost := argCosts[2] - argCost := conditionalCost.Add(ifTrueCost.Union(ifFalseCost)) - return CallEstimate{CostEstimate: argCost, ResultSize: &size} - case overloads.AddString, overloads.AddBytes, overloads.AddList: - if len(args) == 2 { - lhsSize := c.sizeOrUnknown(args[0]) - rhsSize := c.sizeOrUnknown(args[1]) - resultSize := lhsSize.Add(rhsSize) - rhsEntrySize := c.computeEntrySize(args[0].Expr()) - lhsEntrySize := c.computeEntrySize(args[1].Expr()) - resultEntrySize := rhsEntrySize.union(lhsEntrySize) - if resultEntrySize != nil { - c.setEntrySize(e, resultEntrySize) - } - switch overloadID { - case overloads.AddList: - // list concatenation is O(1), but we handle it here to track size - return CallEstimate{CostEstimate: FixedCostEstimate(1).Add(argCostSum()), ResultSize: &resultSize} - default: - return CallEstimate{CostEstimate: resultSize.MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum()), ResultSize: &resultSize} - } - } - case overloads.LessString, overloads.GreaterString, overloads.LessEqualsString, overloads.GreaterEqualsString, - overloads.LessBytes, overloads.GreaterBytes, overloads.LessEqualsBytes, overloads.GreaterEqualsBytes, - overloads.Equals, overloads.NotEquals: - lhsCost := c.sizeOrUnknown(args[0]) - rhsCost := c.sizeOrUnknown(args[1]) - min := uint64(0) - smallestMax := lhsCost.Max - if rhsCost.Max < smallestMax { - smallestMax = rhsCost.Max - } - if smallestMax > 0 { - min = 1 - } - // equality of 2 scalar values results in a cost of 1 - return CallEstimate{ - CostEstimate: CostEstimate{Min: min, Max: smallestMax}.MultiplyByCostFactor(common.StringTraversalCostFactor).Add(argCostSum()), - } - } - // O(1) functions - // See CostTracker.costCall for more details about O(1) cost calculations - - // Benchmarks suggest that most of the other operations take +/- 50% of a base cost unit - // which on an Intel xeon 2.20GHz CPU is 50ns. - return CallEstimate{CostEstimate: FixedCostEstimate(1).Add(argCostSum())} -} - -func (c *coster) getType(e ast.Expr) *types.Type { - return c.checkedAST.GetType(e.ID()) -} - -func (c *coster) getPath(e ast.Expr) []string { - if e.Kind() == ast.IdentKind { - if v, found := c.peekLocalVar(e.AsIdent()); found { - return v.path[:] - } - } - return c.exprPaths[e.ID()][:] -} - -func (c *coster) addPath(e ast.Expr, path []string) { - c.exprPaths[e.ID()] = path -} - -func isAccumulatorVar(name string) bool { - return name == parser.AccumulatorName || name == parser.HiddenAccumulatorName -} - -func (c *coster) newAstNode(e ast.Expr) *astNode { - path := c.getPath(e) - if len(path) > 0 && isAccumulatorVar(path[0]) { - // only provide paths to root vars; omit accumulator vars - path = nil - } - return &astNode{ - path: path, - t: c.getType(e), - expr: e, - derivedSize: c.computeSize(e)} -} - -func (c *coster) setSize(e ast.Expr, size *SizeEstimate) { - if size == nil { - return - } - // Store the computed size with the expression - c.computedSizes[e.ID()] = *size -} - -func (c *coster) sizeOrUnknown(node any) SizeEstimate { - switch v := node.(type) { - case ast.Expr: - if sz := c.computeSize(v); sz != nil { - return *sz - } - case AstNode: - if sz := v.ComputedSize(); sz != nil { - return *sz - } - } - return UnknownSizeEstimate() -} - -func (c *coster) copySizeEstimates(dst, src ast.Expr) { - c.setSize(dst, c.computeSize(src)) - c.setEntrySize(dst, c.computeEntrySize(src)) -} - -func (c *coster) computeSize(e ast.Expr) *SizeEstimate { - if size, ok := c.computedSizes[e.ID()]; ok { - return &size - } - if size := computeExprSize(e); size != nil { - return size - } - // Ensure size estimates are computed first as users may choose to override the costs that - // CEL would otherwise ascribe to the type. - node := astNode{expr: e, path: c.getPath(e), t: c.getType(e)} - if size := c.estimator.EstimateSize(node); size != nil { - // storing the computed size should reduce calls to EstimateSize() - c.computedSizes[e.ID()] = *size - return size - } - if size := computeTypeSize(c.getType(e)); size != nil { - return size - } - if e.Kind() == ast.IdentKind { - varName := e.AsIdent() - if v, ok := c.peekLocalVar(varName); ok && v.size != nil { - return v.size - } - } - return nil -} - -func (c *coster) setEntrySize(e ast.Expr, size *entrySizeEstimate) { - if size == nil { - return - } - c.computedEntrySizes[e.ID()] = *size -} - -func (c *coster) computeEntrySize(e ast.Expr) *entrySizeEstimate { - if sz, found := c.computedEntrySizes[e.ID()]; found { - return &sz - } - if e.Kind() == ast.IdentKind { - varName := e.AsIdent() - if v, ok := c.peekLocalVar(varName); ok && v.entrySize != nil { - return v.entrySize - } - } - return nil -} - -func computeExprSize(expr ast.Expr) *SizeEstimate { - var v uint64 - switch expr.Kind() { - case ast.LiteralKind: - switch ck := expr.AsLiteral().(type) { - case types.String: - // converting to runes here is an O(n) operation, but - // this is consistent with how size is computed at runtime, - // and how the language definition defines string size - v = uint64(len([]rune(ck))) - case types.Bytes: - v = uint64(len(ck)) - case types.Bool, types.Double, types.Duration, - types.Int, types.Timestamp, types.Uint, - types.Null: - v = uint64(1) - default: - return nil - } - case ast.ListKind: - v = uint64(expr.AsList().Size()) - case ast.MapKind: - v = uint64(expr.AsMap().Size()) - default: - return nil - } - cost := FixedSizeEstimate(v) - return &cost -} - -func computeTypeSize(t *types.Type) *SizeEstimate { - if isScalar(t) { - cost := FixedSizeEstimate(1) - return &cost - } - return nil -} - -// isScalar returns true if the given type is known to be of a constant size at -// compile time. isScalar will return false for strings (they are variable-width) -// in addition to protobuf.Any and protobuf.Value (their size is not knowable at compile time). -func isScalar(t *types.Type) bool { - switch t.Kind() { - case types.BoolKind, types.DoubleKind, types.DurationKind, types.IntKind, types.TimestampKind, types.UintKind: - return true - case types.OpaqueKind: - if t.TypeName() == "optional_type" { - return isScalar(t.Parameters()[0]) - } - } - return false -} - -var ( - doubleTwoTo64 = math.Ldexp(1.0, 64) - - unknownSizeEstimate = SizeEstimate{Min: 0, Max: math.MaxUint64} - unknownCostEstimate = unknownSizeEstimate.MultiplyByCostFactor(1) - - selectAndIdentCost = FixedCostEstimate(common.SelectAndIdentCost) - constCost = FixedCostEstimate(common.ConstCost) - - createListBaseCost = FixedCostEstimate(common.ListCreateBaseCost) - createMapBaseCost = FixedCostEstimate(common.MapCreateBaseCost) - createMessageBaseCost = FixedCostEstimate(common.StructCreateBaseCost) -) diff --git a/vendor/github.com/google/cel-go/common/env/io.go b/vendor/github.com/google/cel-go/common/env/io.go deleted file mode 100644 index ec126f9ce67..00000000000 --- a/vendor/github.com/google/cel-go/common/env/io.go +++ /dev/null @@ -1,271 +0,0 @@ -// 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 env - -import ( - "errors" - "fmt" - - "go.yaml.in/yaml/v3" -) - -type internalTypeDesc struct { - TypeName string `yaml:"type_name"` - Params []*TypeDesc `yaml:"params,omitempty"` - IsTypeParam bool `yaml:"is_type_param,omitempty"` -} - -// Embedding TypeDesc in variable causes issues with customizing -// unmarshalling / marshalling. Work around with a parallel type. -type internalVariable struct { - Name string `yaml:"name"` - Description string `yaml:"description,omitempty"` - - // Type represents the type declaration for the variable. - Type *TypeDesc `yaml:"type,omitempty"` - - TypeName string `yaml:"type_name"` - Params []*TypeDesc `yaml:"params,omitempty"` - IsTypeParam bool `yaml:"is_type_param,omitempty"` -} - -// UnmarshalYAML implements yaml.Unmarshal -func (v *Variable) UnmarshalYAML(n *yaml.Node) error { - buf := internalVariable{} - err := n.Decode(&buf) - if err != nil { - return err - } - v.Name = buf.Name - v.Description = buf.Description - if buf.TypeName != "" { - v.TypeDesc = &TypeDesc{ - TypeName: buf.TypeName, - Params: buf.Params, - IsTypeParam: buf.IsTypeParam, - } - } else if buf.Type != nil { - v.TypeDesc = buf.Type - } - return nil -} - -// MarshalYAML implements yaml.Marshaler -func (v *Variable) MarshalYAML() (any, error) { - // The presence of an unmarshaller alters the default marshaller behavior so - // provide a simple marshal implementation. - buf := internalVariable{ - Name: v.Name, - Description: v.Description, - } - if t := v.GetType(); t != nil { - buf.TypeName = t.TypeName - buf.Params = t.Params - buf.IsTypeParam = t.IsTypeParam - } - return &buf, nil -} - -// UnmarshalYAML implements yaml.Unmarshaler -func (td *TypeDesc) UnmarshalYAML(n *yaml.Node) error { - if td == nil { - return fmt.Errorf("unexpected Unmarshal for TypeDesc at: %d", n.Line) - } - if n.Kind == yaml.ScalarNode { - o, err := ParseTypeDesc(n.Value) - if err != nil { - return err - } - *td = *o - return nil - } - - if n.Kind != yaml.MappingNode { - return errors.New("unsupported yaml for TypeDesc") - } - - buf := internalTypeDesc{} - err := n.Decode(&buf) - if err != nil { - return err - } - td.TypeName = buf.TypeName - td.Params = buf.Params - td.IsTypeParam = buf.IsTypeParam - return nil -} - -type typeDescParser struct { - text string - pos int - length int -} - -// ParseTypeDesc parses a TypeDesc from the type specifier format: "map" -func ParseTypeDesc(text string) (*TypeDesc, error) { - p := &typeDescParser{text: text, length: len(text)} - res, err := p.parseTypeElem() - if err != nil { - return nil, fmt.Errorf("failed to parse type %q: %v", text, err) - } - p.skipWhitespace() - if p.pos < p.length { - return nil, fmt.Errorf("unexpected character %q at position %d in %q", p.text[p.pos], p.pos, text) - } - return res, nil -} - -func (p *typeDescParser) parseConcreteType() (*TypeDesc, error) { - id, err := p.parseNamespaceIdentifier() - if err != nil { - return nil, err - } - if p.pos < p.length && p.text[p.pos] == '<' { - p.pos++ // consume '<' - var params []*TypeDesc - for { - p.skipWhitespace() - param, err := p.parseTypeElem() - if err != nil { - return nil, err - } - params = append(params, param) - p.skipWhitespace() - if p.pos < p.length && p.text[p.pos] == ',' { - p.pos++ // consume ',' - continue - } - if p.pos < p.length && p.text[p.pos] == '>' { - p.pos++ // consume '>' - break - } - return nil, fmt.Errorf("expected ',' or '>' at position %d", p.pos) - } - return NewTypeDesc(id, params...), nil - } - return NewTypeDesc(id), nil -} - -func (p *typeDescParser) parseTypeElem() (*TypeDesc, error) { - p.skipWhitespace() - if p.pos < p.length && p.text[p.pos] == '~' { - p.pos++ // consume '~' - id, err := p.parseTypeParamIdent() - if err != nil { - return nil, err - } - return NewTypeParam(id), nil - } - return p.parseConcreteType() -} - -func (p *typeDescParser) parseNamespaceIdentifier() (string, error) { - p.skipWhitespace() - var id string - for p.pos < p.length && p.text[p.pos] != '<' { - c := p.text[p.pos] - if c == '.' { - id += "." - p.pos++ // consume '.' - } - ident, err := p.parseIdentifier() - if err != nil { - return "", err - } - id += ident - p.skipWhitespace() - if p.pos < p.length && p.text[p.pos] != '.' { - break - } - } - if id == "" { - return "", fmt.Errorf("missing identifier at position %d", p.pos) - } - return id, nil -} - -func (p *typeDescParser) parseIdentifier() (string, error) { - p.skipWhitespace() - if p.pos >= p.length { - return "", fmt.Errorf("unexpected end of input") - } - start := p.pos - c := p.text[p.pos] - if !isAlpha(c) && c != '_' { - return "", fmt.Errorf("identifier is expected, but %q was found at position %d", c, p.pos) - } - p.pos++ - for p.pos < p.length { - c := p.text[p.pos] - if !isAlphaNumeric(c) && c != '_' { - break - } - p.pos++ - } - return p.text[start:p.pos], nil -} - -func (p *typeDescParser) parseTypeParamIdent() (string, error) { - p.skipWhitespace() - if p.pos >= p.length { - return "", fmt.Errorf("unexpected end of input") - } - c := p.text[p.pos] - if !isAlpha(c) { - return "", fmt.Errorf("invalid type parameter identifier %q at position %d, must be a single character from A-Z", c, p.pos) - } - p.pos++ - if p.pos < p.length && isAlpha(p.text[p.pos]) { - return "", fmt.Errorf("invalid type param, must have a single alphabetic character at position %d", p.pos) - } - return string(c), nil -} - -func (p *typeDescParser) skipWhitespace() { - for p.pos < p.length && p.text[p.pos] == ' ' { - p.pos++ - } -} - -func isAlpha(c byte) bool { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') -} - -func isAlphaNumeric(c byte) bool { - return isAlpha(c) || (c >= '0' && c <= '9') -} - -// ConfigFromYAML returns a config from YAML source. -// -// Adds custom parsing logic for normalizing shorthand for specifiying some fields -// in a YAML document (mainly the type-specifier shorthand). -// -// Using yaml.Unmarshal with any implementation should be sufficient for most -// cases. -func ConfigFromYAML(data []byte) (*Config, error) { - c := &Config{} - e := yaml.Unmarshal(data, c) - if e != nil { - return nil, e - } - return c, nil -} - -// ConfigToYAML returns the config serialized to YAML -// -// Provided as a convenience wrapper around a tested YAML Marshaler. -func ConfigToYAML(c *Config) ([]byte, error) { - return yaml.Marshal(c) -} diff --git a/vendor/github.com/google/cel-go/common/types/list.go b/vendor/github.com/google/cel-go/common/types/list.go deleted file mode 100644 index 028770ed683..00000000000 --- a/vendor/github.com/google/cel-go/common/types/list.go +++ /dev/null @@ -1,593 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types - -import ( - "fmt" - "reflect" - "strings" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - - anypb "google.golang.org/protobuf/types/known/anypb" - structpb "google.golang.org/protobuf/types/known/structpb" -) - -// NewDynamicList returns a traits.Lister with heterogenous elements. -// value should be an array of "native" types, i.e. any type that -// NativeToValue() can convert to a ref.Val. -func NewDynamicList(adapter Adapter, value any) traits.Lister { - refValue := reflect.ValueOf(value) - return &baseList{ - Adapter: adapter, - value: value, - size: refValue.Len(), - get: func(i int) any { - return refValue.Index(i).Interface() - }, - } -} - -// NewStringList returns a traits.Lister containing only strings. -func NewStringList(adapter Adapter, elems []string) traits.Lister { - return &baseList{ - Adapter: adapter, - value: elems, - size: len(elems), - get: func(i int) any { return elems[i] }, - } -} - -// NewRefValList returns a traits.Lister with ref.Val elements. -// -// This type specialization is used with list literals within CEL expressions. -func NewRefValList(adapter Adapter, elems []ref.Val) traits.Lister { - return &baseList{ - Adapter: adapter, - value: elems, - size: len(elems), - get: func(i int) any { return elems[i] }, - } -} - -// NewProtoList returns a traits.Lister based on a pb.List instance. -func NewProtoList(adapter Adapter, list protoreflect.List) traits.Lister { - return &baseList{ - Adapter: adapter, - value: list, - size: list.Len(), - get: func(i int) any { return list.Get(i).Interface() }, - } -} - -// NewJSONList returns a traits.Lister based on structpb.ListValue instance. -func NewJSONList(adapter Adapter, l *structpb.ListValue) traits.Lister { - vals := l.GetValues() - return &baseList{ - Adapter: adapter, - value: l, - size: len(vals), - get: func(i int) any { return vals[i] }, - } -} - -// NewMutableList creates a new mutable list whose internal state can be modified. -func NewMutableList(adapter Adapter) traits.MutableLister { - var mutableValues []ref.Val - l := &mutableList{ - baseList: &baseList{ - Adapter: adapter, - value: mutableValues, - size: 0, - }, - mutableValues: mutableValues, - } - l.get = func(i int) any { - return l.mutableValues[i] - } - return l -} - -// baseList points to a list containing elements of any type. -// The `value` is an array of native values, and refValue is its reflection object. -// The `Adapter` enables native type to CEL type conversions. -type baseList struct { - Adapter - value any - - // size indicates the number of elements within the list. - // Since objects are immutable the size of a list is static. - size int - - // get returns a value at the specified integer index. - // The index is guaranteed to be checked against the list index range. - get func(int) any -} - -// Add implements the traits.Adder interface method. -func (l *baseList) Add(other ref.Val) ref.Val { - otherList, ok := other.(traits.Lister) - if !ok { - return MaybeNoSuchOverloadErr(other) - } - return newConcatList(l.Adapter, l, otherList) -} - -// Contains implements the traits.Container interface method. -func (l *baseList) Contains(elem ref.Val) ref.Val { - for i := 0; i < l.size; i++ { - val := l.NativeToValue(l.get(i)) - cmp := elem.Equal(val) - b, ok := cmp.(Bool) - if ok && b == True { - return True - } - } - return False -} - -// ConvertToNative implements the ref.Val interface method. -func (l *baseList) ConvertToNative(typeDesc reflect.Type) (any, error) { - if typeDesc == reflect.TypeFor[any]() { - typeDesc = reflect.TypeFor[[]any]() - } - // If the underlying list value is assignable to the reflected type return it. - if reflect.TypeOf(l.value).AssignableTo(typeDesc) { - return l.value, nil - } - // If the list wrapper is assignable to the desired type return it. - if reflect.TypeOf(l).AssignableTo(typeDesc) { - return l, nil - } - // Attempt to convert the list to a set of well known protobuf types. - switch typeDesc { - case anyValueType: - json, err := l.ConvertToNative(JSONListType) - if err != nil { - return nil, err - } - return anypb.New(json.(proto.Message)) - case JSONValueType, JSONListType: - jsonValues, err := - l.ConvertToNative(reflect.TypeOf([]*structpb.Value{})) - if err != nil { - return nil, err - } - jsonList := &structpb.ListValue{Values: jsonValues.([]*structpb.Value)} - if typeDesc == JSONListType { - return jsonList, nil - } - return structpb.NewListValue(jsonList), nil - } - // Non-list conversion. - if typeDesc.Kind() != reflect.Slice && typeDesc.Kind() != reflect.Array { - return nil, fmt.Errorf("type conversion error from list to '%v'", typeDesc) - } - - // List conversion. - // Allow the element ConvertToNative() function to determine whether conversion is possible. - otherElemType := typeDesc.Elem() - elemCount := l.size - var nativeList reflect.Value - if typeDesc.Kind() == reflect.Array { - nativeList = reflect.New(reflect.ArrayOf(elemCount, typeDesc)).Elem().Index(0) - } else { - nativeList = reflect.MakeSlice(typeDesc, elemCount, elemCount) - - } - for i := 0; i < elemCount; i++ { - elem := l.NativeToValue(l.get(i)) - nativeElemVal, err := elem.ConvertToNative(otherElemType) - if err != nil { - return nil, err - } - nativeList.Index(i).Set(reflect.ValueOf(nativeElemVal)) - } - return nativeList.Interface(), nil -} - -// ConvertToType implements the ref.Val interface method. -func (l *baseList) ConvertToType(typeVal ref.Type) ref.Val { - switch typeVal { - case ListType: - return l - case TypeType: - return ListType - } - return NewErr("type conversion error from '%s' to '%s'", ListType, typeVal) -} - -// Equal implements the ref.Val interface method. -func (l *baseList) Equal(other ref.Val) ref.Val { - otherList, ok := other.(traits.Lister) - if !ok { - return False - } - if l.Size() != otherList.Size() { - return False - } - for i := IntZero; i < l.Size().(Int); i++ { - thisElem := l.Get(i) - otherElem := otherList.Get(i) - elemEq := Equal(thisElem, otherElem) - if elemEq == False { - return False - } - } - return True -} - -// Get implements the traits.Indexer interface method. -func (l *baseList) Get(index ref.Val) ref.Val { - ind, err := IndexOrError(index) - if err != nil { - return ValOrErr(index, "%v", err) - } - if ind < 0 || ind >= l.size { - return NewErr("index '%d' out of range in list size '%d'", ind, l.Size()) - } - return l.NativeToValue(l.get(ind)) -} - -// IsZeroValue returns true if the list is empty. -func (l *baseList) IsZeroValue() bool { - return l.size == 0 -} - -// Fold calls the FoldEntry method for each (index, value) pair in the list. -func (l *baseList) Fold(f traits.Folder) { - for i := 0; i < l.size; i++ { - if !f.FoldEntry(i, l.get(i)) { - break - } - } -} - -// Iterator implements the traits.Iterable interface method. -func (l *baseList) Iterator() traits.Iterator { - return newListIterator(l) -} - -// Size implements the traits.Sizer interface method. -func (l *baseList) Size() ref.Val { - return Int(l.size) -} - -// Type implements the ref.Val interface method. -func (l *baseList) Type() ref.Type { - return ListType -} - -// Value implements the ref.Val interface method. -func (l *baseList) Value() any { - return l.value -} - -// String converts the list to a human readable string form. -func (l *baseList) String() string { - var sb strings.Builder - sb.WriteString("[") - for i := 0; i < l.size; i++ { - sb.WriteString(fmt.Sprintf("%v", l.get(i))) - if i != l.size-1 { - sb.WriteString(", ") - } - } - sb.WriteString("]") - return sb.String() -} - -func formatList(l traits.Lister, sb *strings.Builder) { - sb.WriteString("[") - n, _ := l.Size().(Int) - for i := 0; i < int(n); i++ { - formatTo(sb, l.Get(Int(i))) - if i != int(n)-1 { - sb.WriteString(", ") - } - } - sb.WriteString("]") -} - -func (l *baseList) format(sb *strings.Builder) { - formatList(l, sb) -} - -// mutableList aggregates values into its internal storage. For use with internal CEL variables only. -type mutableList struct { - *baseList - mutableValues []ref.Val -} - -// Add copies elements from the other list into the internal storage of the mutable list. -// The ref.Val returned by Add is the receiver. -func (l *mutableList) Add(other ref.Val) ref.Val { - switch otherList := other.(type) { - case *mutableList: - l.mutableValues = append(l.mutableValues, otherList.mutableValues...) - l.size += len(otherList.mutableValues) - case traits.Lister: - for i := IntZero; i < otherList.Size().(Int); i++ { - l.size++ - l.mutableValues = append(l.mutableValues, otherList.Get(i)) - } - default: - return MaybeNoSuchOverloadErr(otherList) - } - return l -} - -// ToImmutableList returns an immutable list based on the internal storage of the mutable list. -func (l *mutableList) ToImmutableList() traits.Lister { - // The reference to internal state is guaranteed to be safe as this call is only performed - // when mutations have been completed. - return NewRefValList(l.Adapter, l.mutableValues) -} - -// concatList combines two list implementations together into a view. -// The `Adapter` enables native type to CEL type conversions. -type concatList struct { - Adapter - value any - prevList traits.Lister - nextList traits.Lister - cachedSize ref.Val -} - -func newConcatList(adapter Adapter, prevList, nextList traits.Lister) ref.Val { - prevSize := prevList.Size().(Int) - nextSize := nextList.Size().(Int) - if prevSize == IntZero { - return nextList.(ref.Val) - } - if nextSize == IntZero { - return prevList.(ref.Val) - } - return &concatList{ - Adapter: adapter, - prevList: prevList, - nextList: nextList, - cachedSize: prevSize.Add(nextSize), - } -} - -// Add implements the traits.Adder interface method. -func (l *concatList) Add(other ref.Val) ref.Val { - otherList, ok := other.(traits.Lister) - if !ok { - return MaybeNoSuchOverloadErr(other) - } - return newConcatList(l.Adapter, l, otherList) -} - -// Contains implements the traits.Container interface method. -func (l *concatList) Contains(elem ref.Val) ref.Val { - // The concat list relies on the IsErrorOrUnknown checks against the input element to be - // performed by the `prevList` and/or `nextList`. - prev := l.prevList.Contains(elem) - // Short-circuit the return if the elem was found in the prev list. - if prev == True { - return prev - } - // Return if the elem was found in the next list. - next := l.nextList.Contains(elem) - if next == True { - return next - } - // Handle the case where an error or unknown was encountered before checking next. - if IsUnknownOrError(prev) { - return prev - } - // Otherwise, rely on the next value as the representative result. - return next -} - -// ConvertToNative implements the ref.Val interface method. -func (l *concatList) ConvertToNative(typeDesc reflect.Type) (any, error) { - combined := NewDynamicList(l.Adapter, l.Value().([]any)) - return combined.ConvertToNative(typeDesc) -} - -// ConvertToType implements the ref.Val interface method. -func (l *concatList) ConvertToType(typeVal ref.Type) ref.Val { - switch typeVal { - case ListType: - return l - case TypeType: - return ListType - } - return NewErr("type conversion error from '%s' to '%s'", ListType, typeVal) -} - -// Equal implements the ref.Val interface method. -func (l *concatList) Equal(other ref.Val) ref.Val { - otherList, ok := other.(traits.Lister) - if !ok { - return False - } - if l.Size() != otherList.Size() { - return False - } - var maybeErr ref.Val - for i := IntZero; i < l.Size().(Int); i++ { - thisElem := l.Get(i) - otherElem := otherList.Get(i) - elemEq := Equal(thisElem, otherElem) - if elemEq == False { - return False - } - if maybeErr == nil && IsUnknownOrError(elemEq) { - maybeErr = elemEq - } - } - if maybeErr != nil { - return maybeErr - } - return True -} - -// Get implements the traits.Indexer interface method. -func (l *concatList) Get(index ref.Val) ref.Val { - ind, err := IndexOrError(index) - if err != nil { - return ValOrErr(index, "%v", err) - } - i := Int(ind) - if i < l.prevList.Size().(Int) { - return l.prevList.Get(i) - } - offset := i - l.prevList.Size().(Int) - return l.nextList.Get(offset) -} - -// IsZeroValue returns true if the list is empty. -func (l *concatList) IsZeroValue() bool { - return l.Size().(Int) == 0 -} - -// Fold calls the FoldEntry method for each (index, value) pair in the list. -func (l *concatList) Fold(f traits.Folder) { - for i := Int(0); i < l.Size().(Int); i++ { - if !f.FoldEntry(i, l.Get(i)) { - break - } - } -} - -// Iterator implements the traits.Iterable interface method. -func (l *concatList) Iterator() traits.Iterator { - return newListIterator(l) -} - -// Size implements the traits.Sizer interface method. -func (l *concatList) Size() ref.Val { - return l.cachedSize -} - -// String converts the concatenated list to a human-readable string. -func (l *concatList) String() string { - var sb strings.Builder - sb.WriteString("[") - for i := Int(0); i < l.Size().(Int); i++ { - sb.WriteString(fmt.Sprintf("%v", l.Get(i))) - if i != l.Size().(Int)-1 { - sb.WriteString(", ") - } - } - sb.WriteString("]") - return sb.String() -} - -// Type implements the ref.Val interface method. -func (l *concatList) Type() ref.Type { - return ListType -} - -// Value implements the ref.Val interface method. -func (l *concatList) Value() any { - if l.value == nil { - merged := make([]any, l.Size().(Int)) - prevLen := l.prevList.Size().(Int) - for i := Int(0); i < prevLen; i++ { - merged[i] = l.prevList.Get(i).Value() - } - nextLen := l.nextList.Size().(Int) - for j := Int(0); j < nextLen; j++ { - merged[prevLen+j] = l.nextList.Get(j).Value() - } - l.value = merged - } - return l.value -} - -func newListIterator(listValue traits.Lister) traits.Iterator { - return &listIterator{ - listValue: listValue, - len: listValue.Size().(Int), - } -} - -type listIterator struct { - *baseIterator - listValue traits.Lister - cursor Int - len Int -} - -// HasNext implements the traits.Iterator interface method. -func (it *listIterator) HasNext() ref.Val { - return Bool(it.cursor < it.len) -} - -// Next implements the traits.Iterator interface method. -func (it *listIterator) Next() ref.Val { - if it.HasNext() == True { - index := it.cursor - it.cursor++ - return it.listValue.Get(index) - } - return nil -} - -// IndexOrError converts an input index value into either a lossless integer index or an error. -func IndexOrError(index ref.Val) (int, error) { - switch iv := index.(type) { - case Int: - return int(iv), nil - case Double: - if ik, ok := doubleToInt64Lossless(float64(iv)); ok { - return int(ik), nil - } - return -1, fmt.Errorf("unsupported index value %v in list", index) - case Uint: - if ik, ok := uint64ToInt64Lossless(uint64(iv)); ok { - return int(ik), nil - } - return -1, fmt.Errorf("unsupported index value %v in list", index) - default: - return -1, fmt.Errorf("unsupported index type '%s' in list", index.Type()) - } -} - -// ToFoldableList will create a Foldable version of a list suitable for key-value pair iteration. -// -// For values which are already Foldable, this call is a no-op. For all other values, the fold is -// driven via the Size() and Get() calls which means that the folding will function, but take a -// performance hit. -func ToFoldableList(l traits.Lister) traits.Foldable { - if f, ok := l.(traits.Foldable); ok { - return f - } - return interopFoldableList{Lister: l} -} - -type interopFoldableList struct { - traits.Lister -} - -// Fold implements the traits.Foldable interface method and performs an iteration over the -// range of elements of the list. -func (l interopFoldableList) Fold(f traits.Folder) { - sz := l.Size().(Int) - for i := Int(0); i < sz; i++ { - if !f.FoldEntry(i, l.Get(i)) { - break - } - } -} diff --git a/vendor/github.com/google/cel-go/common/types/provider.go b/vendor/github.com/google/cel-go/common/types/provider.go deleted file mode 100644 index 1bb2c11ed7d..00000000000 --- a/vendor/github.com/google/cel-go/common/types/provider.go +++ /dev/null @@ -1,837 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types - -import ( - "fmt" - "reflect" - "time" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - - "github.com/google/cel-go/common/types/pb" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - - exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" - anypb "google.golang.org/protobuf/types/known/anypb" - dpb "google.golang.org/protobuf/types/known/durationpb" - structpb "google.golang.org/protobuf/types/known/structpb" - tpb "google.golang.org/protobuf/types/known/timestamppb" -) - -// Adapter converts native Go values of varying type and complexity to equivalent CEL values. -type Adapter = ref.TypeAdapter - -// Provider specifies functions for creating new object instances and for resolving -// enum values by name. -type Provider interface { - // EnumValue returns the numeric value of the given enum value name. - EnumValue(enumName string) ref.Val - - // FindIdent takes a qualified identifier name and returns a ref.Val if one exists. - FindIdent(identName string) (ref.Val, bool) - - // FindStructType returns the Type give a qualified type name. - // - // For historical reasons, only struct types are expected to be returned through this - // method, and the type values are expected to be wrapped in a TypeType instance using - // TypeTypeWithParam(). - // - // Returns false if not found. - FindStructType(structType string) (*Type, bool) - - // FindStructFieldNames returns thet field names associated with the type, if the type - // is found. - FindStructFieldNames(structType string) ([]string, bool) - - // FieldStructFieldType returns the field type for a checked type value. Returns - // false if the field could not be found. - FindStructFieldType(structType, fieldName string) (*FieldType, bool) - - // NewValue creates a new type value from a qualified name and map of field - // name to value. - // - // Note, for each value, the Val.ConvertToNative function will be invoked - // to convert the Val to the field's native type. If an error occurs during - // conversion, the NewValue will be a types.Err. - NewValue(structType string, fields map[string]ref.Val) ref.Val -} - -// FieldType represents a field's type value and whether that field supports presence detection. -type FieldType struct { - // Type of the field as a CEL native type value. - Type *Type - - // IsSet indicates whether the field is set on an input object. - IsSet ref.FieldTester - - // GetFrom retrieves the field value on the input object, if set. - GetFrom ref.FieldGetter - - // IsJSONField - IsJSONField bool -} - -// Registry provides type information for a set of registered types. -type Registry struct { - revTypeMap map[string]*Type - pbdb *pb.Db -} - -// NewRegistry accepts a list of proto message instances and returns a type -// provider which can create new instances of the provided message or any -// message that proto depends upon in its FileDescriptor. -func NewRegistry(types ...proto.Message) (*Registry, error) { - return NewProtoRegistry(ProtoTypeDefs(types...)) -} - -// RegistryOption configures the behavior of the registry. -type RegistryOption func(r *Registry) (*Registry, error) - -// JSONFieldNames configures JSON field name support within the protobuf types in the registry. -func JSONFieldNames(enabled bool) RegistryOption { - return func(r *Registry) (*Registry, error) { - err := r.WithJSONFieldNames(enabled) - return r, err - } -} - -// ProtoTypeDefs creates a RegistryOption which registers the individual proto messages with the registry. -func ProtoTypeDefs(types ...proto.Message) RegistryOption { - return func(r *Registry) (*Registry, error) { - for _, msgType := range types { - err := r.RegisterMessage(msgType) - if err != nil { - return nil, err - } - } - return r, nil - } -} - -// NewProtoRegistry creates a proto-based registry with a set of configurable options. -func NewProtoRegistry(opts ...RegistryOption) (*Registry, error) { - r := &Registry{ - revTypeMap: make(map[string]*Type), - pbdb: pb.NewDb(), - } - err := r.RegisterType( - BoolType, - BytesType, - DoubleType, - DurationType, - IntType, - ListType, - MapType, - NullType, - StringType, - TimestampType, - TypeType, - UintType) - if err != nil { - return nil, err - } - // This block ensures that the well-known protobuf types are registered by default. - for _, fd := range r.pbdb.FileDescriptions() { - err = r.registerAllTypes(fd) - if err != nil { - return nil, err - } - } - for _, opt := range opts { - r, err = opt(r) - if err != nil { - return nil, err - } - } - return r, nil -} - -// NewEmptyRegistry returns a registry which is completely unconfigured. -func NewEmptyRegistry() *Registry { - return &Registry{ - revTypeMap: make(map[string]*Type), - pbdb: pb.NewDb(), - } -} - -// Copy copies the current state of the registry into its own memory space. -func (p *Registry) Copy() *Registry { - copy := &Registry{ - revTypeMap: make(map[string]*Type), - pbdb: p.pbdb.Copy(), - } - for k, v := range p.revTypeMap { - copy.revTypeMap[k] = v - } - return copy -} - -// JSONFieldNames returns whether json field names are enabled in this registry. -func (p *Registry) JSONFieldNames() bool { - return p.pbdb.JSONFieldNames() -} - -// WithJSONFieldNames configures the registry with the JSON field name support enabled or disabled. -func (p *Registry) WithJSONFieldNames(enabled bool) error { - if enabled == p.pbdb.JSONFieldNames() { - return nil - } - newDB := pb.NewDb(pb.JSONFieldNames(enabled)) - files := p.pbdb.FileDescriptions() - for _, fd := range files { - _, err := newDB.RegisterDescriptor(fd.FileDescriptor()) - if err != nil { - return err - } - } - p.pbdb = newDB - return nil -} - -// EnumValue returns the numeric value of the given enum value name. -func (p *Registry) EnumValue(enumName string) ref.Val { - enumVal, found := p.pbdb.DescribeEnum(enumName) - if !found { - return NewErr("unknown enum name '%s'", enumName) - } - return Int(enumVal.Value()) -} - -// FindFieldType returns the field type for a checked type value. Returns false if -// the field could not be found. -// -// Deprecated: use FindStructFieldType -func (p *Registry) FindFieldType(structType, fieldName string) (*ref.FieldType, bool) { - msgType, found := p.pbdb.DescribeType(structType) - if !found { - return nil, false - } - field, found := msgType.FieldByName(fieldName) - if !found { - return nil, false - } - return &ref.FieldType{ - Type: field.CheckedType(), - IsSet: field.IsSet, - GetFrom: field.GetFrom, - IsJSONField: p.pbdb.JSONFieldNames() && fieldName == field.JSONName(), - }, true -} - -// FindStructFieldNames returns the set of field names for the given struct type, -// if the type exists in the registry. -func (p *Registry) FindStructFieldNames(structType string) ([]string, bool) { - msgType, found := p.pbdb.DescribeType(structType) - if !found { - return []string{}, false - } - fieldMap := msgType.FieldMap() - fields := make([]string, len(fieldMap)) - idx := 0 - for f := range fieldMap { - fields[idx] = f - idx++ - } - return fields, true -} - -// FindStructFieldType returns the field type for a checked type value. Returns -// false if the field could not be found. -func (p *Registry) FindStructFieldType(structType, fieldName string) (*FieldType, bool) { - msgType, found := p.pbdb.DescribeType(structType) - if !found { - return nil, false - } - field, found := msgType.FieldByName(fieldName) - if !found { - return nil, false - } - return &FieldType{ - Type: fieldDescToCELType(field), - IsSet: field.IsSet, - GetFrom: field.GetFrom, - IsJSONField: p.pbdb.JSONFieldNames() && fieldName == field.JSONName(), - }, true -} - -// FindStructFieldDescription returns documentation for a field if available. -// Returns false if the field could not be found. -func (p *Registry) FindStructFieldDescription(structType, fieldName string) (string, bool) { - msgType, found := p.pbdb.DescribeType(structType) - if !found { - return "", false - } - field, found := msgType.FieldByName(fieldName) - if !found { - return "", false - } - return field.Documentation(), true -} - -// FindIdent takes a qualified identifier name and returns a ref.Val if one exists. -func (p *Registry) FindIdent(identName string) (ref.Val, bool) { - if t, found := p.revTypeMap[identName]; found { - return t, true - } - if enumVal, found := p.pbdb.DescribeEnum(identName); found { - return Int(enumVal.Value()), true - } - return nil, false -} - -// FindType looks up the Type given a qualified typeName. Returns false if not found. -// -// Deprecated: use FindStructType -func (p *Registry) FindType(structType string) (*exprpb.Type, bool) { - if _, found := p.pbdb.DescribeType(structType); !found { - return nil, false - } - if structType != "" && structType[0] == '.' { - structType = structType[1:] - } - return &exprpb.Type{ - TypeKind: &exprpb.Type_Type{ - Type: &exprpb.Type{ - TypeKind: &exprpb.Type_MessageType{ - MessageType: structType}}}}, true -} - -// FindStructType returns the Type give a qualified type name. -// -// For historical reasons, only struct types are expected to be returned through this -// method, and the type values are expected to be wrapped in a TypeType instance using -// TypeTypeWithParam(). -// -// Returns false if not found. -func (p *Registry) FindStructType(structType string) (*Type, bool) { - if _, found := p.pbdb.DescribeType(structType); !found { - return nil, false - } - if structType != "" && structType[0] == '.' { - structType = structType[1:] - } - return NewTypeTypeWithParam(NewObjectType(structType)), true -} - -// NewValue creates a new type value from a qualified name and map of field -// name to value. -// -// Note, for each value, the Val.ConvertToNative function will be invoked -// to convert the Val to the field's native type. If an error occurs during -// conversion, the NewValue will be a types.Err. -func (p *Registry) NewValue(structType string, fields map[string]ref.Val) ref.Val { - td, found := p.pbdb.DescribeType(structType) - if !found { - return NewErr("unknown type '%s'", structType) - } - msg := td.New() - for name, value := range fields { - field, found := td.FieldByName(name) - if !found { - return NewErr("no such field: %s", name) - } - err := msgSetField(msg, field, value) - if err != nil { - return &Err{error: err} - } - } - return p.NativeToValue(msg.Interface()) -} - -// RegisterDescriptor registers the contents of a protocol buffer `FileDescriptor`. -func (p *Registry) RegisterDescriptor(fileDesc protoreflect.FileDescriptor) error { - fd, err := p.pbdb.RegisterDescriptor(fileDesc) - if err != nil { - return err - } - return p.registerAllTypes(fd) -} - -// RegisterMessage registers a protocol buffer message and its dependencies. -func (p *Registry) RegisterMessage(message proto.Message) error { - fd, err := p.pbdb.RegisterMessage(message) - if err != nil { - return err - } - return p.registerAllTypes(fd) -} - -// RegisterType registers a type value with the provider which ensures the provider is aware of how to -// map the type to an identifier. -// -// If the `ref.Type` value is a `*types.Type` it will be registered directly by its runtime type name. -// If the `ref.Type` value is not a `*types.Type` instance, a `*types.Type` instance which reflects the -// traits present on the input and the runtime type name. By default this foreign type will be treated -// as a types.StructKind. To avoid potential issues where the `ref.Type` values does not match the -// generated `*types.Type` instance, consider always using the `*types.Type` to represent type extensions -// to CEL, even when they're not based on protobuf types. -func (p *Registry) RegisterType(types ...ref.Type) error { - for _, t := range types { - celType := maybeForeignType(t) - existing, found := p.revTypeMap[t.TypeName()] - if !found { - p.revTypeMap[t.TypeName()] = celType - continue - } - if !existing.IsEquivalentType(celType) { - return fmt.Errorf("type registration conflict. found: %v, input: %v", existing, celType) - } - if existing.traitMask != celType.traitMask { - return fmt.Errorf( - "type registered with conflicting traits: %v with traits %v, input: %v", - existing.TypeName(), existing.traitMask, celType.traitMask) - } - } - return nil -} - -// NativeToValue converts various "native" types to ref.Val with this specific implementation -// providing support for custom proto-based types. -// -// This method should be the inverse of ref.Val.ConvertToNative. -func (p *Registry) NativeToValue(value any) ref.Val { - if val, found := nativeToValue(p, value); found { - return val - } - switch v := value.(type) { - case proto.Message: - typeName := string(v.ProtoReflect().Descriptor().FullName()) - td, found := p.pbdb.DescribeType(typeName) - if !found { - return NewErr("unknown type: '%s'", typeName) - } - unwrapped, isUnwrapped, err := td.MaybeUnwrap(v) - if err != nil { - return UnsupportedRefValConversionErr(v) - } - if isUnwrapped { - return p.NativeToValue(unwrapped) - } - typeVal, found := p.FindIdent(typeName) - if !found { - return NewErr("unknown type: '%s'", typeName) - } - return NewObject(p, td, typeVal, v) - case *pb.Map: - return NewProtoMap(p, v) - case protoreflect.List: - return NewProtoList(p, v) - case protoreflect.Message: - return p.NativeToValue(v.Interface()) - case protoreflect.Value: - return p.NativeToValue(v.Interface()) - } - return UnsupportedRefValConversionErr(value) -} - -func (p *Registry) registerAllTypes(fd *pb.FileDescription) error { - for _, typeName := range fd.GetTypeNames() { - // skip well-known type names since they're automatically sanitized - // during NewObjectType() calls. - if _, found := checkedWellKnowns[typeName]; found { - continue - } - err := p.RegisterType(NewObjectTypeValue(typeName)) - if err != nil { - return err - } - } - return nil -} - -func fieldDescToCELType(field *pb.FieldDescription) *Type { - if field.IsMap() { - return NewMapType( - singularFieldDescToCELType(field.KeyType), - singularFieldDescToCELType(field.ValueType)) - } - if field.IsList() { - return NewListType(singularFieldDescToCELType(field)) - } - return singularFieldDescToCELType(field) -} - -func singularFieldDescToCELType(field *pb.FieldDescription) *Type { - if field.IsMessage() { - return NewObjectType(string(field.Descriptor().Message().FullName())) - } - if field.IsEnum() { - return IntType - } - return ProtoCELPrimitives[field.ProtoKind()] -} - -// defaultTypeAdapter converts go native types to CEL values. -type defaultTypeAdapter struct{} - -var ( - // DefaultTypeAdapter adapts canonical CEL types from their equivalent Go values. - DefaultTypeAdapter = &defaultTypeAdapter{} -) - -// NativeToValue implements the ref.TypeAdapter interface. -func (a *defaultTypeAdapter) NativeToValue(value any) ref.Val { - if val, found := nativeToValue(a, value); found { - return val - } - return UnsupportedRefValConversionErr(value) -} - -// nativeToValue returns the converted (ref.Val, true) of a conversion is found, -// otherwise (nil, false) -func nativeToValue(a Adapter, value any) (ref.Val, bool) { - switch v := value.(type) { - case nil: - return NullValue, true - case *Bool: - if v != nil { - return *v, true - } - case *Bytes: - if v != nil { - return *v, true - } - case *Double: - if v != nil { - return *v, true - } - case *Int: - if v != nil { - return *v, true - } - case *String: - if v != nil { - return *v, true - } - case *Uint: - if v != nil { - return *v, true - } - case bool: - return Bool(v), true - case int: - return Int(v), true - case int32: - return Int(v), true - case int64: - return Int(v), true - case uint: - return Uint(v), true - case uint32: - return Uint(v), true - case uint64: - return Uint(v), true - case float32: - return Double(v), true - case float64: - return Double(v), true - case string: - return String(v), true - case *dpb.Duration: - return Duration{Duration: v.AsDuration()}, true - case time.Duration: - return Duration{Duration: v}, true - case *tpb.Timestamp: - return Timestamp{Time: v.AsTime()}, true - case time.Time: - return Timestamp{Time: v}, true - case *bool: - if v != nil { - return Bool(*v), true - } - case *float32: - if v != nil { - return Double(*v), true - } - case *float64: - if v != nil { - return Double(*v), true - } - case *int: - if v != nil { - return Int(*v), true - } - case *int32: - if v != nil { - return Int(*v), true - } - case *int64: - if v != nil { - return Int(*v), true - } - case *string: - if v != nil { - return String(*v), true - } - case *uint: - if v != nil { - return Uint(*v), true - } - case *uint32: - if v != nil { - return Uint(*v), true - } - case *uint64: - if v != nil { - return Uint(*v), true - } - case []byte: - return Bytes(v), true - // specializations for common lists types. - case []string: - return NewStringList(a, v), true - case []ref.Val: - return NewRefValList(a, v), true - // specializations for common map types. - case map[string]string: - return NewStringStringMap(a, v), true - case map[string]any: - return NewStringInterfaceMap(a, v), true - case map[ref.Val]ref.Val: - return NewRefValMap(a, v), true - // additional specializations may be added upon request / need. - case *anypb.Any: - if v == nil { - return UnsupportedRefValConversionErr(v), true - } - unpackedAny, err := v.UnmarshalNew() - if err != nil { - return NewErr("anypb.UnmarshalNew() failed for type %q: %v", v.GetTypeUrl(), err), true - } - return a.NativeToValue(unpackedAny), true - case *structpb.NullValue, structpb.NullValue: - return NullValue, true - case *structpb.ListValue: - return NewJSONList(a, v), true - case *structpb.Struct: - return NewJSONStruct(a, v), true - case ref.Val: - return v, true - case protoreflect.EnumNumber: - return Int(v), true - case proto.Message: - if v == nil { - return UnsupportedRefValConversionErr(v), true - } - typeName := string(v.ProtoReflect().Descriptor().FullName()) - td, found := pb.DefaultDb.DescribeType(typeName) - if !found { - return nil, false - } - val, unwrapped, err := td.MaybeUnwrap(v) - if err != nil { - return UnsupportedRefValConversionErr(v), true - } - if !unwrapped { - return nil, false - } - return a.NativeToValue(val), true - // Note: dynamicpb.Message implements the proto.Message _and_ protoreflect.Message interfaces - // which means that this case must appear after handling a proto.Message type. - case protoreflect.Message: - return a.NativeToValue(v.Interface()), true - default: - refValue := reflect.ValueOf(v) - if refValue.Kind() == reflect.Ptr { - if refValue.IsNil() { - return UnsupportedRefValConversionErr(v), true - } - refValue = refValue.Elem() - } - refKind := refValue.Kind() - switch refKind { - case reflect.Array, reflect.Slice: - if refValue.Type().Elem() == reflect.TypeOf(byte(0)) { - if refValue.CanAddr() { - return Bytes(refValue.Bytes()), true - } - tmp := reflect.New(refValue.Type()) - tmp.Elem().Set(refValue) - return Bytes(tmp.Elem().Bytes()), true - } - return NewDynamicList(a, v), true - case reflect.Map: - return NewDynamicMap(a, v), true - // type aliases of primitive types cannot be asserted as that type, but rather need - // to be downcast to int32 before being converted to a CEL representation. - case reflect.Bool: - boolTupe := reflect.TypeOf(false) - return Bool(refValue.Convert(boolTupe).Interface().(bool)), true - case reflect.Int: - intType := reflect.TypeOf(int(0)) - return Int(refValue.Convert(intType).Interface().(int)), true - case reflect.Int8: - intType := reflect.TypeOf(int8(0)) - return Int(refValue.Convert(intType).Interface().(int8)), true - case reflect.Int16: - intType := reflect.TypeOf(int16(0)) - return Int(refValue.Convert(intType).Interface().(int16)), true - case reflect.Int32: - intType := reflect.TypeOf(int32(0)) - return Int(refValue.Convert(intType).Interface().(int32)), true - case reflect.Int64: - intType := reflect.TypeOf(int64(0)) - return Int(refValue.Convert(intType).Interface().(int64)), true - case reflect.Uint: - uintType := reflect.TypeOf(uint(0)) - return Uint(refValue.Convert(uintType).Interface().(uint)), true - case reflect.Uint8: - uintType := reflect.TypeOf(uint8(0)) - return Uint(refValue.Convert(uintType).Interface().(uint8)), true - case reflect.Uint16: - uintType := reflect.TypeOf(uint16(0)) - return Uint(refValue.Convert(uintType).Interface().(uint16)), true - case reflect.Uint32: - uintType := reflect.TypeOf(uint32(0)) - return Uint(refValue.Convert(uintType).Interface().(uint32)), true - case reflect.Uint64: - uintType := reflect.TypeOf(uint64(0)) - return Uint(refValue.Convert(uintType).Interface().(uint64)), true - case reflect.Float32: - doubleType := reflect.TypeOf(float32(0)) - return Double(refValue.Convert(doubleType).Interface().(float32)), true - case reflect.Float64: - doubleType := reflect.TypeOf(float64(0)) - return Double(refValue.Convert(doubleType).Interface().(float64)), true - case reflect.String: - stringType := reflect.TypeOf("") - return String(refValue.Convert(stringType).Interface().(string)), true - } - } - return nil, false -} - -func msgSetField(target protoreflect.Message, field *pb.FieldDescription, val ref.Val) error { - if field.IsList() { - lv := target.NewField(field.Descriptor()) - list, ok := val.(traits.Lister) - if !ok { - return unsupportedTypeConversionError(field, val) - } - err := msgSetListField(lv.List(), field, list) - if err != nil { - return err - } - target.Set(field.Descriptor(), lv) - return nil - } - if field.IsMap() { - mv := target.NewField(field.Descriptor()) - mp, ok := val.(traits.Mapper) - if !ok { - return unsupportedTypeConversionError(field, val) - } - err := msgSetMapField(mv.Map(), field, mp) - if err != nil { - return err - } - target.Set(field.Descriptor(), mv) - return nil - } - v, err := val.ConvertToNative(field.ReflectType()) - if err != nil { - return fieldTypeConversionError(field, err) - } - if v == nil { - return nil - } - switch pv := v.(type) { - case proto.Message: - v = pv.ProtoReflect() - } - target.Set(field.Descriptor(), protoreflect.ValueOf(v)) - return nil -} - -func msgSetListField(target protoreflect.List, listField *pb.FieldDescription, listVal traits.Lister) error { - elemReflectType := listField.ReflectType().Elem() - for i := Int(0); i < listVal.Size().(Int); i++ { - elem := listVal.Get(i) - elemVal, err := elem.ConvertToNative(elemReflectType) - if err != nil { - return fieldTypeConversionError(listField, err) - } - if elemVal == nil { - continue - } - switch ev := elemVal.(type) { - case proto.Message: - elemVal = ev.ProtoReflect() - } - target.Append(protoreflect.ValueOf(elemVal)) - } - return nil -} - -func msgSetMapField(target protoreflect.Map, mapField *pb.FieldDescription, mapVal traits.Mapper) error { - targetKeyType := mapField.KeyType.ReflectType() - targetValType := mapField.ValueType.ReflectType() - it := mapVal.Iterator() - for it.HasNext() == True { - key := it.Next() - val := mapVal.Get(key) - k, err := key.ConvertToNative(targetKeyType) - if err != nil { - return fieldTypeConversionError(mapField, err) - } - v, err := val.ConvertToNative(targetValType) - if err != nil { - return fieldTypeConversionError(mapField, err) - } - if v == nil { - continue - } - switch pv := v.(type) { - case proto.Message: - v = pv.ProtoReflect() - } - target.Set(protoreflect.ValueOf(k).MapKey(), protoreflect.ValueOf(v)) - } - return nil -} - -func unsupportedTypeConversionError(field *pb.FieldDescription, val ref.Val) error { - msgName := field.Descriptor().ContainingMessage().FullName() - return fmt.Errorf("unsupported field type for %v.%v: %v", msgName, field.Name(), val.Type()) -} - -func fieldTypeConversionError(field *pb.FieldDescription, err error) error { - msgName := field.Descriptor().ContainingMessage().FullName() - return fmt.Errorf("field type conversion error for %v.%v value type: %v", msgName, field.Name(), err) -} - -var ( - // ProtoCELPrimitives provides a map from the protoreflect Kind to the equivalent CEL type. - ProtoCELPrimitives = map[protoreflect.Kind]*Type{ - protoreflect.BoolKind: BoolType, - protoreflect.BytesKind: BytesType, - protoreflect.DoubleKind: DoubleType, - protoreflect.FloatKind: DoubleType, - protoreflect.Int32Kind: IntType, - protoreflect.Int64Kind: IntType, - protoreflect.Sint32Kind: IntType, - protoreflect.Sint64Kind: IntType, - protoreflect.Uint32Kind: UintType, - protoreflect.Uint64Kind: UintType, - protoreflect.Fixed32Kind: UintType, - protoreflect.Fixed64Kind: UintType, - protoreflect.Sfixed32Kind: IntType, - protoreflect.Sfixed64Kind: IntType, - protoreflect.StringKind: StringType, - } -) diff --git a/vendor/github.com/google/cel-go/ext/costs.go b/vendor/github.com/google/cel-go/ext/costs.go deleted file mode 100644 index d2cf7c757f8..00000000000 --- a/vendor/github.com/google/cel-go/ext/costs.go +++ /dev/null @@ -1,122 +0,0 @@ -// 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 ext - -import ( - "math" - - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" -) - -var ( - callCostEstimate = checker.FixedCostEstimate(1) - callCost = uint64(1) - listAllocCost = checker.FixedCostEstimate(common.ListCreateBaseCost) - stringCostFactor = common.StringTraversalCostFactor -) - -func estimateStringScan(sz checker.SizeEstimate) (checker.CostEstimate, *checker.SizeEstimate) { - return estimateTraversal(sz, stringCostFactor, nil) -} - -func estimateListAlloc(sz checker.SizeEstimate, costFactor float64) (checker.CostEstimate, *checker.SizeEstimate) { - return estimateTraversal(sz, costFactor, &listAllocCost) -} - -// estimateTraversal computes cost as a function of the size of the target object and whether the call allocates memory. -func estimateTraversal(nodeSize checker.SizeEstimate, costFactor float64, allocationCost *checker.CostEstimate) (checker.CostEstimate, *checker.SizeEstimate) { - cost := nodeSize.MultiplyByCostFactor(costFactor) - if allocationCost != nil { - cost = cost.Add(*allocationCost) - } - return cost, &nodeSize -} - -func estimateSize(estimator checker.CostEstimator, node checker.AstNode) checker.SizeEstimate { - if l := node.ComputedSize(); l != nil { - return *l - } - if l := estimator.EstimateSize(node); l != nil { - return *l - } - return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} -} - -func actualSize(value ref.Val) uint64 { - if sz, ok := value.(traits.Sizer); ok { - return uint64(sz.Size().(types.Int)) - } - return 1 -} - -func nodeAsUintValue(node checker.AstNode, defaultVal uint64) uint64 { - if node.Expr().Kind() != ast.LiteralKind { - return defaultVal - } - lit := node.Expr().AsLiteral() - if lit.Type() != types.IntType { - return defaultVal - } - val := lit.(types.Int) - if val < types.IntZero { - return 0 - } - return uint64(lit.(types.Int)) -} - -func callEstimate(cost checker.CostEstimate, sz *checker.SizeEstimate) *checker.CallEstimate { - return &checker.CallEstimate{CostEstimate: cost, ResultSize: sz} -} - -func rangedSizeEstimate(min, max uint64) checker.SizeEstimate { - return checker.SizeEstimate{Min: min, Max: max} -} - -func fixedSizeEstimate(val uint64) checker.SizeEstimate { - return checker.FixedSizeEstimate(val) -} - -func atLeastOne(size checker.SizeEstimate) checker.SizeEstimate { - if size.Min == 0 { - size.Min = 1 - } - if size.Max == 0 { - size.Max = 1 - } - return size -} - -func safeAdd(x, y uint64, rest ...uint64) uint64 { - if y > 0 && x > math.MaxUint64-y { - return math.MaxUint64 - } - next := x + y - if len(rest) == 0 { - return next - } - return safeAdd(next, rest[0], rest[1:]...) -} - -func safeMul(x, y uint64) uint64 { - if y != 0 && x > math.MaxUint64/y { - return math.MaxUint64 - } - return x * y -} diff --git a/vendor/github.com/google/cel-go/ext/encoders.go b/vendor/github.com/google/cel-go/ext/encoders.go deleted file mode 100644 index 10beea4f135..00000000000 --- a/vendor/github.com/google/cel-go/ext/encoders.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package 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 -// encodings. -// -// # Base64.Decode -// -// Decodes base64-encoded string to bytes. -// -// This function will return an error if the string input is not base64-encoded. -// -// base64.decode() -> -// -// Examples: -// -// base64.decode('aGVsbG8=') // return b'hello' -// base64.decode('aGVsbG8') // return b'hello' -// -// # Base64.Encode -// -// Encodes bytes to a base64-encoded string. -// -// base64.encode() -> -// -// Examples: -// -// base64.encode(b'hello') // return b'aGVsbG8=' -// -// # JSON.Encode -// -// Introduced at version: 1 -// -// Encodes a CEL value to a JSON string. -// -// json.encode() -> -// -// Examples: -// -// json.encode({'hello': 'world'}) // return '{"hello":"world"}' -func Encoders(options ...EncodersOption) cel.EnvOption { - l := &encoderLib{version: math.MaxUint32} - for _, o := range options { - l = o(l) - } - return cel.Lib(l) -} - -// EncodersOption declares a functional operator for configuring encoder extensions. -type EncodersOption func(*encoderLib) *encoderLib - -// EncodersVersion sets the library version for encoder extensions. -func EncodersVersion(version uint32) EncodersOption { - return func(lib *encoderLib) *encoderLib { - lib.version = version - return lib - } -} - -type encoderLib struct { - version uint32 -} - -func (*encoderLib) LibraryName() string { - return "cel.lib.ext.encoders" -} - -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 { - s := str.(types.String) - return bytesOrError(base64DecodeString(string(s))) - }))), - cel.Function("base64.encode", - cel.Overload("base64_encode_bytes", []*cel.Type{cel.BytesType}, cel.StringType, - cel.UnaryBinding(func(bytes ref.Val) ref.Val { - b := bytes.(types.Bytes) - 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 { - return []cel.ProgramOption{} -} - -func base64DecodeString(str string) ([]byte, error) { - b, err := base64.StdEncoding.DecodeString(str) - if err == nil { - return b, nil - } - if _, tryAltEncoding := err.(base64.CorruptInputError); tryAltEncoding { - return base64.RawStdEncoding.DecodeString(str) - } - return nil, err -} - -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 -} diff --git a/vendor/github.com/google/cel-go/ext/extension_option_factory.go b/vendor/github.com/google/cel-go/ext/extension_option_factory.go deleted file mode 100644 index e68cf5bc773..00000000000 --- a/vendor/github.com/google/cel-go/ext/extension_option_factory.go +++ /dev/null @@ -1,92 +0,0 @@ -// 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 ext - -import ( - "fmt" - - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/env" -) - -// ExtensionOptionFactory converts an ExtensionConfig value to a CEL environment option. -func ExtensionOptionFactory(configElement any) (cel.EnvOption, bool) { - ext, isExtension := configElement.(*env.Extension) - if !isExtension { - return nil, false - } - name := ext.Name - alias, found := extAliases[name] - if found { - name = alias - } - fac, found := extFactories[name] - if !found { - return nil, false - } - // If the version is 'latest', set the version value to the max uint. - ver, err := ext.VersionNumber() - if err != nil { - return func(*cel.Env) (*cel.Env, error) { - return nil, fmt.Errorf("invalid extension version: %s - %s", ext.Name, ext.Version) - }, true - } - return fac(ver), true -} - -// extensionFactory accepts a version and produces a CEL environment associated with the versioned extension. -type extensionFactory func(uint32) cel.EnvOption - -var extFactories = map[string]extensionFactory{ - "cel.lib.ext.cel.bindings": func(version uint32) cel.EnvOption { - return Bindings(BindingsVersion(version)) - }, - "cel.lib.ext.encoders": func(version uint32) cel.EnvOption { - return Encoders(EncodersVersion(version)) - }, - "cel.lib.ext.lists": func(version uint32) cel.EnvOption { - return Lists(ListsVersion(version)) - }, - "cel.lib.ext.math": func(version uint32) cel.EnvOption { - return Math(MathVersion(version)) - }, - "cel.lib.ext.protos": func(version uint32) cel.EnvOption { - return Protos(ProtosVersion(version)) - }, - "cel.lib.ext.sets": func(version uint32) cel.EnvOption { - return Sets(SetsVersion(version)) - }, - "cel.lib.ext.strings": func(version uint32) cel.EnvOption { - return Strings(StringsVersion(version)) - }, - "cel.lib.ext.comprev2": func(version uint32) cel.EnvOption { - return TwoVarComprehensions(TwoVarComprehensionsVersion(version)) - }, - "cel.lib.ext.regex": func(version uint32) cel.EnvOption { - return Regex(RegexVersion(version)) - }, -} - -var extAliases = map[string]string{ - "bindings": "cel.lib.ext.cel.bindings", - "encoders": "cel.lib.ext.encoders", - "lists": "cel.lib.ext.lists", - "math": "cel.lib.ext.math", - "protos": "cel.lib.ext.protos", - "sets": "cel.lib.ext.sets", - "strings": "cel.lib.ext.strings", - "two-var-comprehensions": "cel.lib.ext.comprev2", - "regex": "cel.lib.ext.regex", -} diff --git a/vendor/github.com/google/cel-go/ext/lists.go b/vendor/github.com/google/cel-go/ext/lists.go deleted file mode 100644 index b4ac2589959..00000000000 --- a/vendor/github.com/google/cel-go/ext/lists.go +++ /dev/null @@ -1,787 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ext - -import ( - "fmt" - "math" - "sort" - - "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker" - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/decls" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/interpreter" - "github.com/google/cel-go/parser" -) - -var comparableTypes = []*cel.Type{ - cel.IntType, - cel.UintType, - cel.DoubleType, - cel.BoolType, - cel.DurationType, - cel.TimestampType, - cel.StringType, - cel.BytesType, -} - -// Lists returns a cel.EnvOption to configure extended functions for list manipulation. -// As a general note, all indices are zero-based. -// -// # Distinct -// -// Introduced in version: 2 (cost support in version 3) -// -// Returns the distinct elements of a list. -// -// .distinct() -> -// -// Examples: -// -// [1, 2, 2, 3, 3, 3].distinct() // return [1, 2, 3] -// ["b", "b", "c", "a", "c"].distinct() // return ["b", "c", "a"] -// [1, "b", 2, "b"].distinct() // return [1, "b", 2] -// -// # Range -// -// Introduced in version: 2 (cost support in version 3) -// -// Returns a list of integers from 0 to n-1. -// -// lists.range() -> -// -// Examples: -// -// lists.range(5) -> [0, 1, 2, 3, 4] -// -// # Reverse -// -// Introduced in version: 2 (cost support in version 3) -// -// Returns the elements of a list in reverse order. -// -// .reverse() -> -// -// Examples: -// -// [5, 3, 1, 2].reverse() // return [2, 1, 3, 5] -// -// # Slice -// -// Introduced in version: 0 (cost support in version 3) -// -// Returns a new sub-list using the indexes provided. -// -// .slice(, ) -> -// -// Examples: -// -// [1,2,3,4].slice(1, 3) // return [2, 3] -// [1,2,3,4].slice(2, 4) // return [3 ,4] -// -// # Flatten -// -// Introduced in version: 1 (cost support in version 3) -// -// Flattens a list recursively. -// If an optional depth is provided, the list is flattened to a the specified level. -// A negative depth value will result in an error. -// -// .flatten() -> -// .flatten() -> -// -// Examples: -// -// [1,[2,3],[4]].flatten() // return [1, 2, 3, 4] -// [1,[2,[3,4]]].flatten() // return [1, 2, [3, 4]] -// [1,2,[],[],[3,4]].flatten() // return [1, 2, 3, 4] -// [1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]] -// [1,[2,[3,[4]]]].flatten(-1) // error -// -// # Sort -// -// Introduced in version: 2 (cost support in version 3) -// -// Sorts a list with comparable elements. If the element type is not comparable -// or the element types are not the same, the function will produce an error. -// -// .sort() -> -// T in {int, uint, double, bool, duration, timestamp, string, bytes} -// -// Examples: -// -// [3, 2, 1].sort() // return [1, 2, 3] -// ["b", "c", "a"].sort() // return ["a", "b", "c"] -// [1, "b"].sort() // error -// [[1, 2, 3]].sort() // error -// -// # SortBy -// -// Introduced in version: 2 (cost support in version 3) -// -// Sorts a list by a key value, i.e., the order is determined by the result of -// an expression applied to each element of the list. -// The output of the key expression must be a comparable type, otherwise the -// function will return an error. -// -// .sortBy(, ) -> -// keyExpr returns a value in {int, uint, double, bool, duration, timestamp, string, bytes} -// -// Examples: -// -// [ -// Player { name: "foo", score: 0 }, -// Player { name: "bar", score: -10 }, -// Player { name: "baz", score: 1000 }, -// ].sortBy(e, e.score).map(e, e.name) -// == ["bar", "foo", "baz"] -func Lists(options ...ListsOption) cel.EnvOption { - 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 - maxRangeSize int64 -} - -// LibraryName implements the SingletonLibrary interface method. -func (listsLib) LibraryName() string { - return "cel.lib.ext.lists" -} - -// ListsOption is a functional interface for configuring the strings library. -type ListsOption func(*listsLib) *listsLib - -// ListsVersion configures the version of the string library. -// -// The version limits which functions are available. Only functions introduced -// below or equal to the given version included in the library. If this option -// is not set, all functions are available. -// -// See the library documentation to determine which version a function was introduced. -// If the documentation does not state which version a function was introduced, it can -// be assumed to be introduced at version 0, when the library was first created. -func ListsVersion(version uint32) ListsOption { - return func(lib *listsLib) *listsLib { - lib.version = version - return lib - } -} - -// 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")) - listListType := cel.ListType(listType) - listDyn := cel.ListType(cel.DynType) - opts := []cel.EnvOption{ - cel.Function("slice", - cel.MemberOverload("list_slice", - []*cel.Type{listType, cel.IntType, cel.IntType}, listType, - cel.FunctionBinding(func(args ...ref.Val) ref.Val { - list := args[0].(traits.Lister) - start := args[1].(types.Int) - end := args[2].(types.Int) - result, err := slice(list, start, end) - if err != nil { - return types.WrapErr(err) - } - return result - }), - ), - ), - } - if lib.version >= 1 { - opts = append(opts, - cel.Function("flatten", - cel.MemberOverload("list_flatten", - []*cel.Type{listListType}, listType, - cel.UnaryBinding(func(arg ref.Val) ref.Val { - // double-check as type-guards disabled - list, ok := arg.(traits.Lister) - if !ok { - return types.ValOrErr(arg, "no such overload: %v.flatten()", arg.Type()) - } - flatList, err := flatten(list, 1) - if err != nil { - return types.WrapErr(err) - } - - return types.DefaultTypeAdapter.NativeToValue(flatList) - }), - ), - cel.MemberOverload("list_flatten_int", - []*cel.Type{listDyn, types.IntType}, listDyn, - cel.BinaryBinding(func(arg1, arg2 ref.Val) ref.Val { - // double-check as type-guards disabled - list, ok := arg1.(traits.Lister) - if !ok { - return types.ValOrErr(arg1, "no such overload: %v.flatten(%v)", arg1.Type(), arg2.Type()) - } - depth, ok := arg2.(types.Int) - if !ok { - return types.ValOrErr(arg1, "no such overload: %v.flatten(%v)", arg1.Type(), arg2.Type()) - } - flatList, err := flatten(list, int64(depth)) - if err != nil { - return types.WrapErr(err) - } - - return types.DefaultTypeAdapter.NativeToValue(flatList) - }), - ), - // To handle the case where a variable of just `list(T)` is provided at runtime - // with a graceful failure more, disable the type guards since the implementation - // can handle lists which are already flat. - decls.DisableTypeGuards(true), - ), - ) - } - if lib.version >= 2 { - sortDecl := cel.Function("sort", - append( - templatedOverloads(comparableTypes, func(t *cel.Type) cel.FunctionOpt { - return cel.MemberOverload( - fmt.Sprintf("list_%s_sort", t.TypeName()), - []*cel.Type{cel.ListType(t)}, cel.ListType(t), - ) - }), - cel.SingletonUnaryBinding( - func(arg ref.Val) ref.Val { - // validated by type-guards - list := arg.(traits.Lister) - sorted, err := sortList(list) - if err != nil { - return types.WrapErr(err) - } - - return sorted - }, - // List traits - traits.ListerType, - ), - )..., - ) - opts = append(opts, sortDecl) - opts = append(opts, cel.Macros(cel.ReceiverMacro("sortBy", 2, sortByMacro))) - opts = append(opts, cel.Function("@sortByAssociatedKeys", - append( - templatedOverloads(comparableTypes, func(u *cel.Type) cel.FunctionOpt { - return cel.MemberOverload( - fmt.Sprintf("list_%s_sortByAssociatedKeys", u.TypeName()), - []*cel.Type{listType, cel.ListType(u)}, listType, - ) - }), - cel.SingletonBinaryBinding( - func(arg1, arg2 ref.Val) ref.Val { - // validated by type-guards - list := arg1.(traits.Lister) - keys := arg2.(traits.Lister) - sorted, err := sortListByAssociatedKeys(list, keys) - if err != nil { - return types.WrapErr(err) - } - - return sorted - }, - // List traits - traits.ListerType, - ), - )..., - )) - - 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), maxRange) - if err != nil { - return types.WrapErr(err) - } - return result - }), - ), - )) - opts = append(opts, cel.Function("reverse", - cel.MemberOverload("list_reverse", - []*cel.Type{listType}, listType, - cel.UnaryBinding(func(list ref.Val) ref.Val { - result, err := reverseList(list.(traits.Lister)) - if err != nil { - return types.WrapErr(err) - } - return result - }), - ), - )) - opts = append(opts, cel.Function("distinct", - cel.MemberOverload("list_distinct", - []*cel.Type{listType}, listType, - cel.UnaryBinding(func(list ref.Val) ref.Val { - result, err := distinctList(list.(traits.Lister)) - if err != nil { - return types.WrapErr(err) - } - return result - }), - ), - )) - } - if lib.version >= 3 { - estimators := []checker.CostOption{ - checker.OverloadCostEstimate("list_slice", estimateListSlice), - checker.OverloadCostEstimate("list_flatten", estimateListFlatten), - checker.OverloadCostEstimate("list_flatten_int", estimateListFlatten), - checker.OverloadCostEstimate("lists_range", estimateListsRange), - checker.OverloadCostEstimate("list_reverse", estimateListReverse), - checker.OverloadCostEstimate("list_distinct", estimateListDistinct), - } - for _, t := range comparableTypes { - estimators = append(estimators, - checker.OverloadCostEstimate( - fmt.Sprintf("list_%s_sort", t.TypeName()), - estimateListSort(t), - ), - checker.OverloadCostEstimate( - fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), - estimateListSortBy(t), - ), - ) - } - opts = append(opts, cel.CostEstimatorOptions(estimators...)) - } - - return opts -} - -// ProgramOptions implements the Library interface method. -func (lib *listsLib) ProgramOptions() []cel.ProgramOption { - var opts []cel.ProgramOption - if lib.version >= 3 { - // TODO: Add cost trackers for list operations - trackers := []interpreter.CostTrackerOption{ - interpreter.OverloadCostTracker("list_slice", trackListOutputSize), - interpreter.OverloadCostTracker("list_flatten", trackListFlatten), - interpreter.OverloadCostTracker("list_flatten_int", trackListFlatten), - interpreter.OverloadCostTracker("lists_range", trackListOutputSize), - interpreter.OverloadCostTracker("list_reverse", trackListOutputSize), - interpreter.OverloadCostTracker("list_distinct", trackListDistinct), - } - for _, t := range comparableTypes { - trackers = append(trackers, - interpreter.OverloadCostTracker( - fmt.Sprintf("list_%s_sort", t.TypeName()), - trackListSort, - ), - interpreter.OverloadCostTracker( - fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), - trackListSortBy, - ), - ) - } - opts = append(opts, cel.CostTrackerOptions(trackers...)) - } - return opts -} - -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) - } - return types.DefaultTypeAdapter.NativeToValue(newList), nil -} - -func reverseList(list traits.Lister) (ref.Val, error) { - var newList []ref.Val - listLength := list.Size().(types.Int) - for i := types.Int(0); i < listLength; i++ { - val := list.Get(listLength - i - 1) - newList = append(newList, val) - } - return types.DefaultTypeAdapter.NativeToValue(newList), nil -} - -func slice(list traits.Lister, start, end types.Int) (ref.Val, error) { - listLength := list.Size().(types.Int) - if start < 0 || end < 0 { - return nil, fmt.Errorf("cannot slice(%d, %d), negative indexes not supported", start, end) - } - if start > end { - return nil, fmt.Errorf("cannot slice(%d, %d), start index must be less than or equal to end index", start, end) - } - if listLength < end { - return nil, fmt.Errorf("cannot slice(%d, %d), list is length %d", start, end, listLength) - } - - var newList []ref.Val - for i := types.Int(start); i < end; i++ { - val := list.Get(i) - newList = append(newList, val) - } - return types.DefaultTypeAdapter.NativeToValue(newList), nil -} - -func flatten(list traits.Lister, depth int64) ([]ref.Val, error) { - if depth < 0 { - return nil, fmt.Errorf("level must be non-negative") - } - - var newList []ref.Val - iter := list.Iterator() - - for iter.HasNext() == types.True { - val := iter.Next() - nestedList, isList := val.(traits.Lister) - - if !isList || depth == 0 { - newList = append(newList, val) - continue - } else { - flattenedList, err := flatten(nestedList, depth-1) - if err != nil { - return nil, err - } - - newList = append(newList, flattenedList...) - } - } - - return newList, nil -} - -func sortList(list traits.Lister) (ref.Val, error) { - return sortListByAssociatedKeys(list, list) -} - -// Internal function used for the implementation of sort() and sortBy(). -// -// Sorts a list of arbitrary elements, according to the order produced by sorting -// another list of comparable elements. If the element type of the keys is not -// comparable or the element types are not the same, the function will produce an error. -// -// .@sortByAssociatedKeys() -> -// U in {int, uint, double, bool, duration, timestamp, string, bytes} -// -// Example: -// -// ["foo", "bar", "baz"].@sortByAssociatedKeys([3, 1, 2]) // return ["bar", "baz", "foo"] -func sortListByAssociatedKeys(list, keys traits.Lister) (ref.Val, error) { - listLength := list.Size().(types.Int) - keysLength := keys.Size().(types.Int) - if listLength != keysLength { - return nil, fmt.Errorf( - "@sortByAssociatedKeys() expected a list of the same size as the associated keys list, but got %d and %d elements respectively", - listLength, - keysLength, - ) - } - if listLength == 0 { - return list, nil - } - elem := keys.Get(types.IntZero) - if _, ok := elem.(traits.Comparer); !ok { - return nil, fmt.Errorf("list elements must be comparable") - } - - sortedIndices := make([]ref.Val, 0, listLength) - for i := types.IntZero; i < listLength; i++ { - sortedIndices = append(sortedIndices, i) - } - - var err error - sort.Slice(sortedIndices, func(i, j int) bool { - iKey := keys.Get(sortedIndices[i]) - jKey := keys.Get(sortedIndices[j]) - if iKey.Type() != elem.Type() || jKey.Type() != elem.Type() { - err = fmt.Errorf("list elements must have the same type") - return false - } - return iKey.(traits.Comparer).Compare(jKey) == types.IntNegOne - }) - if err != nil { - return nil, err - } - - sorted := make([]ref.Val, 0, listLength) - for _, sortedIdx := range sortedIndices { - sorted = append(sorted, list.Get(sortedIdx)) - } - return types.DefaultTypeAdapter.NativeToValue(sorted), nil -} - -// sortByMacro transforms an expression like: -// -// mylistExpr.sortBy(e, -math.abs(e)) -// -// into something equivalent to: -// -// cel.bind( -// __sortBy_input__, -// myListExpr, -// __sortBy_input__.@sortByAssociatedKeys(__sortBy_input__.map(e, -math.abs(e)) -// ) -func sortByMacro(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) { - varIdent := meh.NewIdent("@__sortBy_input__") - varName := varIdent.AsIdent() - - targetKind := target.Kind() - if targetKind != ast.ListKind && - targetKind != ast.SelectKind && - targetKind != ast.IdentKind && - targetKind != ast.ComprehensionKind && - targetKind != ast.CallKind { - return nil, meh.NewError(target.ID(), "sortBy can only be applied to a list, identifier, comprehension, call or select expression") - } - - mapCompr, err := parser.MakeMap(meh, meh.Copy(varIdent), args) - if err != nil { - return nil, err - } - callExpr := meh.NewMemberCall("@sortByAssociatedKeys", - meh.Copy(varIdent), - mapCompr, - ) - - bindExpr := meh.NewComprehension( - meh.NewList(), - "#unused", - varName, - target, - meh.NewLiteral(types.False), - varIdent, - callExpr, - ) - - return bindExpr, nil -} - -func distinctList(list traits.Lister) (ref.Val, error) { - listLength := list.Size().(types.Int) - if listLength == 0 { - return list, nil - } - uniqueList := make([]ref.Val, 0, listLength) - for i := types.IntZero; i < listLength; i++ { - val := list.Get(i) - seen := false - for j := types.IntZero; j < types.Int(len(uniqueList)); j++ { - if i == j { - continue - } - other := uniqueList[j] - if val.Equal(other) == types.True { - seen = true - break - } - } - if !seen { - uniqueList = append(uniqueList, val) - } - } - - return types.DefaultTypeAdapter.NativeToValue(uniqueList), nil -} - -func templatedOverloads(types []*cel.Type, template func(t *cel.Type) cel.FunctionOpt) []cel.FunctionOpt { - overloads := make([]cel.FunctionOpt, len(types)) - for i, t := range types { - overloads[i] = template(t) - } - return overloads -} - -// estimateListSlice computes an O(n) slice operation with a cost factor of 1. -func estimateListSlice(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) != 2 { - return nil - } - sz := estimateSize(estimator, *target) - start := nodeAsUintValue(args[0], 0) - end := nodeAsUintValue(args[1], sz.Max) - return estimateAllocatingListCall(1, checker.FixedSizeEstimate(end-start)) -} - -// estimateListsRange computes an O(n) range operation with a cost factor of 1. -func estimateListsRange(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target != nil || len(args) != 1 { - return nil - } - return estimateAllocatingListCall(1, checker.FixedSizeEstimate(nodeAsUintValue(args[0], math.MaxUint))) -} - -// estimateListReverse computes an O(n) reverse operation with a cost factor of 1. -func estimateListReverse(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) != 0 { - return nil - } - return estimateAllocatingListCall(1, estimateSize(estimator, *target)) -} - -// estimateListFlatten computes an O(n) flatten operation with a cost factor proportional to the flatten depth. -func estimateListFlatten(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) > 1 { - return nil - } - depth := uint64(1) - if len(args) == 1 { - depth = nodeAsUintValue(args[0], math.MaxUint) - } - return estimateAllocatingListCall(float64(depth), estimateSize(estimator, *target)) -} - -// Compute an O(n^2) with a cost factor of 2, equivalent to sets.contains with a result list -// which can vary in size from 1 element to the original list size. -func estimateListDistinct(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) != 0 { - return nil - } - sz := estimateSize(estimator, *target) - costFactor := 2.0 - return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) -} - -// estimateListSort computes an O(n^2) sort operation with a cost factor of 2 for the equality -// operations against the elements in the list against themselves which occur during the sort computation. -func estimateListSort(t *types.Type) checker.FunctionEstimator { - return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) != 0 { - return nil - } - return estimateListSortCost(estimator, *target, t) - } -} - -// estimateListSortBy computes an O(n^2) sort operation with a cost factor of 2 for the equality -// operations against the sort index list which occur during the sort computation. -func estimateListSortBy(u *types.Type) checker.FunctionEstimator { - return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { - if target == nil || len(args) != 1 { - return nil - } - // Estimate the size of the list used as the sort index - return estimateListSortCost(estimator, args[0], u) - } -} - -// estimateListSortCost estimates an O(n^2) sort operation with a cost factor of 2 for the equality -// operations which occur during the sort computation. -func estimateListSortCost(estimator checker.CostEstimator, node checker.AstNode, elemType *types.Type) *checker.CallEstimate { - sz := estimateSize(estimator, node) - costFactor := 2.0 - switch elemType { - case types.StringType, types.BytesType: - costFactor += common.StringTraversalCostFactor - } - return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) -} - -// estimateAllocatingListCall computes cost as a function of the size of the result list with a -// baseline cost for the call dispatch and the associated list allocation. -func estimateAllocatingListCall(costFactor float64, listSize checker.SizeEstimate) *checker.CallEstimate { - return estimateListCall(costFactor, listSize, true) -} - -// estimateListCall computes cost as a function of the size of the target list and whether the -// call allocates memory. -func estimateListCall(costFactor float64, listSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { - cost := listSize.MultiplyByCostFactor(costFactor).Add(callCostEstimate) - if allocates { - cost = cost.Add(checker.FixedCostEstimate(common.ListCreateBaseCost)) - } - return &checker.CallEstimate{CostEstimate: cost, ResultSize: &listSize} -} - -// trackListOutputSize computes cost as a function of the size of the result list. -func trackListOutputSize(_ []ref.Val, result ref.Val) *uint64 { - return trackAllocatingListCall(1, actualSize(result)) -} - -// trackListFlatten computes cost as a function of the size of the result list and the depth of -// the flatten operation. -func trackListFlatten(args []ref.Val, _ ref.Val) *uint64 { - depth := 1.0 - if len(args) == 2 { - depth = float64(args[1].(types.Int)) - } - inputSize := actualSize(args[0]) - return trackAllocatingListCall(depth, inputSize) -} - -// trackListDistinct computes costs as a worst-case O(n^2) operation over the input list. -func trackListDistinct(args []ref.Val, _ ref.Val) *uint64 { - return trackListSelfCompare(args[0].(traits.Lister)) -} - -// trackListSort computes costs as a worst-case O(n^2) operation over the input list. -func trackListSort(args []ref.Val, result ref.Val) *uint64 { - return trackListSelfCompare(args[0].(traits.Lister)) -} - -// trackListSortBy computes costs as a worst-case O(n^2) operation over the sort index list. -func trackListSortBy(args []ref.Val, result ref.Val) *uint64 { - return trackListSelfCompare(args[1].(traits.Lister)) -} - -// trackListSelfCompare computes costs as a worst-case O(n^2) operation over the input list. -func trackListSelfCompare(l traits.Lister) *uint64 { - sz := actualSize(l) - costFactor := 2.0 - if sz == 0 { - return trackAllocatingListCall(costFactor, 0) - } - elem := l.Get(types.IntZero) - if elem.Type() == types.StringType || elem.Type() == types.BytesType { - costFactor += common.StringTraversalCostFactor - } - return trackAllocatingListCall(costFactor, safeMul(sz, sz)) -} - -// trackAllocatingListCall computes costs as a function of the size of the result list with a baseline cost -// for the call dispatch and the associated list allocation. -func trackAllocatingListCall(costFactor float64, size uint64) *uint64 { - if costFactor < 0.0 { - costFactor = 1.0 - } - cost := safeAdd(uint64(float64(size)*costFactor), callCost, common.ListCreateBaseCost) - return &cost -} diff --git a/vendor/github.com/google/cel-go/ext/native.go b/vendor/github.com/google/cel-go/ext/native.go deleted file mode 100644 index c30f26ad312..00000000000 --- a/vendor/github.com/google/cel-go/ext/native.go +++ /dev/null @@ -1,810 +0,0 @@ -// Copyright 2022 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ext - -import ( - "errors" - "fmt" - "math" - "reflect" - "strings" - "time" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/pb" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" - - structpb "google.golang.org/protobuf/types/known/structpb" -) - -var ( - nativeObjTraitMask = traits.FieldTesterType | traits.IndexerType - jsonValueType = reflect.TypeOf(&structpb.Value{}) - jsonStructType = reflect.TypeOf(&structpb.Struct{}) -) - -// NativeTypes creates a type provider which uses reflect.Type and reflect.Value instances -// to produce type definitions that can be used within CEL. -// -// All struct types in Go are exposed to CEL via their simple package name and struct type name: -// -// ```go -// package identity -// -// type Account struct { -// ID int -// } -// -// ``` -// -// The type `identity.Account` would be exported to CEL using the same qualified name, e.g. -// `identity.Account{ID: 1234}` would create a new `Account` instance with the `ID` field -// populated. -// -// Only exported fields are exposed via NativeTypes, and the type-mapping between Go and CEL -// is as follows: -// -// | Go type | CEL type | -// |-------------------------------------|-----------| -// | bool | bool | -// | []byte | bytes | -// | float32, float64 | double | -// | int, int8, int16, int32, int64 | int | -// | string | string | -// | uint, uint8, uint16, uint32, uint64 | uint | -// | time.Duration | duration | -// | time.Time | timestamp | -// | array, slice | list | -// | map | map | -// -// Please note, if you intend to configure support for proto messages in addition to native -// types, you will need to provide the protobuf types before the golang native types. The -// same advice holds if you are using custom type adapters and type providers. The native type -// provider composes over whichever type adapter and provider is configured in the cel.Env at -// the time that it is invoked. -// -// There is also the possibility to rename the fields of native structs by setting the `cel` tag -// for fields you want to override. In order to enable this feature, pass in the `ParseStructTags(true)` -// option. Here is an example to see it in action: -// -// ```go -// package identity -// -// type Account struct { -// ID int -// OwnerName string `cel:"owner"` -// } -// -// ``` -// -// The `OwnerName` field is now accessible in CEL via `owner`, e.g. `identity.Account{owner: 'bob'}`. -// In case there are duplicated field names in the struct, an error will be returned. -func NativeTypes(args ...any) cel.EnvOption { - return func(env *cel.Env) (*cel.Env, error) { - nativeTypes := make([]any, 0, len(args)) - tpOptions := nativeTypeOptions{ - version: math.MaxUint32, - } - - for _, v := range args { - switch v := v.(type) { - case NativeTypesOption: - err := v(&tpOptions) - if err != nil { - return nil, err - } - default: - nativeTypes = append(nativeTypes, v) - } - } - - tp, err := newNativeTypeProvider(tpOptions, env.CELTypeAdapter(), env.CELTypeProvider(), nativeTypes...) - if err != nil { - return nil, err - } - - env, err = cel.CustomTypeAdapter(tp)(env) - if err != nil { - return nil, err - } - return cel.CustomTypeProvider(tp)(env) - } -} - -// NativeTypesOption is a functional interface for configuring handling of native types. -type NativeTypesOption func(*nativeTypeOptions) error - -// NativeTypesVersion sets the native types version support for native extensions functions. -func NativeTypesVersion(version uint32) NativeTypesOption { - return func(opts *nativeTypeOptions) error { - opts.version = version - return nil - } -} - -// NativeTypesFieldNameHandler is a handler for mapping a reflect.StructField to a CEL field name. -// This can be used to override the default Go struct field to CEL field name mapping. -type NativeTypesFieldNameHandler = func(field reflect.StructField) string - -func fieldNameByTag(structTagToParse string) func(field reflect.StructField) string { - return func(field reflect.StructField) string { - tag, found := field.Tag.Lookup(structTagToParse) - if found { - splits := strings.Split(tag, ",") - if len(splits) > 0 { - // We make the assumption that the leftmost entry in the tag is the name. - // This seems to be true for most tags that have the concept of a name/key, such as: - // https://pkg.go.dev/encoding/xml#Marshal - // https://pkg.go.dev/encoding/json#Marshal - // https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs - // https://pkg.go.dev/go.yaml.in/yaml/v3#Marshal - name := splits[0] - return name - } - } - - return field.Name - } -} - -type nativeTypeOptions struct { - // fieldNameHandler controls how CEL should perform struct field renames. - // This is most commonly used for switching to parsing based off the struct field tag, - // such as "cel" or "json". - fieldNameHandler NativeTypesFieldNameHandler - - // version is the native types library version. - version uint32 -} - -// ParseStructTags configures if native types field names should be overridable by CEL struct tags. -// This is equivalent to ParseStructTag("cel") -func ParseStructTags(enabled bool) NativeTypesOption { - return func(ntp *nativeTypeOptions) error { - if enabled { - ntp.fieldNameHandler = fieldNameByTag("cel") - } else { - ntp.fieldNameHandler = nil - } - return nil - } -} - -// ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field. -// For example: -// If the tag to parse is "cel" and the struct field has tag cel:"foo", the CEL struct field will be "foo". -// If the tag to parse is "json" and the struct field has tag json:"foo,omitempty", the CEL struct field will be "foo". -func ParseStructTag(tag string) NativeTypesOption { - return func(ntp *nativeTypeOptions) error { - ntp.fieldNameHandler = fieldNameByTag(tag) - return nil - } -} - -// ParseStructField configures how to parse Go struct fields. It can be used to customize struct field parsing. -func ParseStructField(handler NativeTypesFieldNameHandler) NativeTypesOption { - return func(ntp *nativeTypeOptions) error { - ntp.fieldNameHandler = handler - return nil - } -} - -func newNativeTypeProvider(tpOptions nativeTypeOptions, adapter types.Adapter, provider types.Provider, refTypes ...any) (*nativeTypeProvider, error) { - nativeTypes := make(map[string]*nativeType, len(refTypes)) - for _, refType := range refTypes { - switch rt := refType.(type) { - case reflect.Type: - result, err := newNativeTypes(tpOptions.fieldNameHandler, rt) - if err != nil { - return nil, err - } - for idx := range result { - nativeTypes[result[idx].TypeName()] = result[idx] - } - case reflect.Value: - result, err := newNativeTypes(tpOptions.fieldNameHandler, rt.Type()) - if err != nil { - return nil, err - } - for idx := range result { - nativeTypes[result[idx].TypeName()] = result[idx] - } - default: - return nil, fmt.Errorf("unsupported native type: %v (%T) must be reflect.Type or reflect.Value", rt, rt) - } - } - return &nativeTypeProvider{ - nativeTypes: nativeTypes, - baseAdapter: adapter, - baseProvider: provider, - options: tpOptions, - }, nil -} - -type nativeTypeProvider struct { - nativeTypes map[string]*nativeType - baseAdapter types.Adapter - baseProvider types.Provider - options nativeTypeOptions -} - -// EnumValue proxies to the types.Provider configured at the times the NativeTypes -// option was configured. -func (tp *nativeTypeProvider) EnumValue(enumName string) ref.Val { - return tp.baseProvider.EnumValue(enumName) -} - -// FindIdent looks up natives type instances by qualified identifier, and if not found -// proxies to the composed types.Provider. -func (tp *nativeTypeProvider) FindIdent(typeName string) (ref.Val, bool) { - if t, found := tp.nativeTypes[typeName]; found { - return t, true - } - return tp.baseProvider.FindIdent(typeName) -} - -// FindStructType looks up the CEL type definition by qualified identifier, and if not found -// proxies to the composed types.Provider. -func (tp *nativeTypeProvider) FindStructType(typeName string) (*types.Type, bool) { - if _, found := tp.nativeTypes[typeName]; found { - return types.NewTypeTypeWithParam(types.NewObjectType(typeName)), true - } - if celType, found := tp.baseProvider.FindStructType(typeName); found { - return celType, true - } - return tp.baseProvider.FindStructType(typeName) -} - -func toFieldName(fieldNameHandler NativeTypesFieldNameHandler, f reflect.StructField) string { - if fieldNameHandler == nil { - return f.Name - } - - return fieldNameHandler(f) -} - -// FindStructFieldNames looks up the type definition first from the native types, then from -// the backing provider type set. If found, a set of field names corresponding to the type -// will be returned. -func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) { - if t, found := tp.nativeTypes[typeName]; found { - fieldCount := t.refType.NumField() - fields := make([]string, fieldCount) - for i := 0; i < fieldCount; i++ { - fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i)) - } - return fields, true - } - if celTypeFields, found := tp.baseProvider.FindStructFieldNames(typeName); found { - return celTypeFields, true - } - return tp.baseProvider.FindStructFieldNames(typeName) -} - -// FindStructFieldType looks up a native type's field definition, and if the type name is not a native -// type then proxies to the composed types.Provider -func (tp *nativeTypeProvider) FindStructFieldType(typeName, fieldName string) (*types.FieldType, bool) { - t, found := tp.nativeTypes[typeName] - if !found { - return tp.baseProvider.FindStructFieldType(typeName, fieldName) - } - refField, isDefined := t.hasField(fieldName) - if !found || !isDefined { - return nil, false - } - celType, ok := convertToCelType(refField.Type) - if !ok { - return nil, false - } - return &types.FieldType{ - Type: celType, - IsSet: func(obj any) bool { - refVal := reflect.Indirect(reflect.ValueOf(obj)) - refField := refVal.FieldByName(refField.Name) - return !refField.IsZero() - }, - GetFrom: func(obj any) (any, error) { - refVal := reflect.Indirect(reflect.ValueOf(obj)) - refField := refVal.FieldByName(refField.Name) - return getFieldValue(refField), nil - }, - }, true -} - -// NewValue implements the ref.TypeProvider interface method. -func (tp *nativeTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val { - t, found := tp.nativeTypes[typeName] - if !found { - return tp.baseProvider.NewValue(typeName, fields) - } - refPtr := reflect.New(t.refType) - refVal := refPtr.Elem() - for fieldName, val := range fields { - refFieldDef, isDefined := t.hasField(fieldName) - if !isDefined { - return types.NewErr("no such field: %s", fieldName) - } - fieldVal, err := val.ConvertToNative(refFieldDef.Type) - if err != nil { - return types.NewErrFromString(err.Error()) - } - refField := refVal.FieldByIndex(refFieldDef.Index) - refFieldVal := reflect.ValueOf(fieldVal) - refField.Set(refFieldVal) - } - return tp.NativeToValue(refPtr.Interface()) -} - -// NewValue adapts native values to CEL values and will proxy to the composed type adapter -// for non-native types. -func (tp *nativeTypeProvider) NativeToValue(val any) ref.Val { - if val == nil { - return types.NullValue - } - if v, ok := val.(ref.Val); ok { - return v - } - rawVal := reflect.ValueOf(val) - refVal := rawVal - if refVal.Kind() == reflect.Ptr { - refVal = reflect.Indirect(refVal) - } - // This isn't quite right if you're also supporting proto, - // but maybe an acceptable limitation. - switch refVal.Kind() { - case reflect.Array, reflect.Slice: - switch val := val.(type) { - case []byte: - return tp.baseAdapter.NativeToValue(val) - default: - if refVal.Type().Elem() == reflect.TypeOf(byte(0)) { - return tp.baseAdapter.NativeToValue(val) - } - return types.NewDynamicList(tp, val) - } - case reflect.Map: - return types.NewDynamicMap(tp, val) - case reflect.Struct: - switch val := val.(type) { - case proto.Message, *pb.Map, protoreflect.List, protoreflect.Message, protoreflect.Value, - time.Time: - return tp.baseAdapter.NativeToValue(val) - default: - return tp.newNativeObject(val, rawVal) - } - default: - return tp.baseAdapter.NativeToValue(val) - } -} - -func convertToCelType(refType reflect.Type) (*cel.Type, bool) { - switch refType.Kind() { - case reflect.Bool: - return cel.BoolType, true - case reflect.Float32, reflect.Float64: - return cel.DoubleType, true - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if refType == durationType { - return cel.DurationType, true - } - return cel.IntType, true - case reflect.String: - return cel.StringType, true - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return cel.UintType, true - case reflect.Array, reflect.Slice: - refElem := refType.Elem() - if refElem == reflect.TypeOf(byte(0)) { - return cel.BytesType, true - } - elemType, ok := convertToCelType(refElem) - if !ok { - return nil, false - } - return cel.ListType(elemType), true - case reflect.Map: - keyType, ok := convertToCelType(refType.Key()) - if !ok { - return nil, false - } - // Ensure the key type is a int, bool, uint, string - elemType, ok := convertToCelType(refType.Elem()) - if !ok { - return nil, false - } - return cel.MapType(keyType, elemType), true - case reflect.Struct: - if refType == timestampType { - return cel.TimestampType, true - } - if refType.Implements(refValType) { - emptyCelVal := reflect.New(refType).Elem().Interface().(ref.Val) - return emptyCelVal.Type().(*cel.Type), true - } - return cel.ObjectType( - fmt.Sprintf("%s.%s", simplePkgAlias(refType.PkgPath()), refType.Name()), - ), true - case reflect.Pointer: - if refType.Implements(refValType) { - emptyCelVal := reflect.New(refType.Elem()).Interface().(ref.Val) - return emptyCelVal.Type().(*cel.Type), true - } - if refType.Implements(pbMsgInterfaceType) { - pbMsg := reflect.New(refType.Elem()).Interface().(protoreflect.ProtoMessage) - return cel.ObjectType(string(pbMsg.ProtoReflect().Descriptor().FullName())), true - } - return convertToCelType(refType.Elem()) - } - return nil, false -} - -func (tp *nativeTypeProvider) newNativeObject(val any, refValue reflect.Value) ref.Val { - valType, err := newNativeType(tp.options.fieldNameHandler, refValue.Type()) - if err != nil { - return types.NewErrFromString(err.Error()) - } - return &nativeObj{ - Adapter: tp, - val: val, - valType: valType, - refValue: refValue, - } -} - -type nativeObj struct { - types.Adapter - val any - valType *nativeType - refValue reflect.Value -} - -// ConvertToNative implements the ref.Val interface method. -// -// CEL does not have a notion of pointers, so whether a field is a pointer or value -// is handled as part of this conversion step. -func (o *nativeObj) ConvertToNative(typeDesc reflect.Type) (any, error) { - if o.refValue.Type() == typeDesc { - return o.val, nil - } - if o.refValue.Kind() == reflect.Pointer && o.refValue.Type().Elem() == typeDesc { - return o.refValue.Elem().Interface(), nil - } - if typeDesc.Kind() == reflect.Pointer && o.refValue.Type() == typeDesc.Elem() { - ptr := reflect.New(typeDesc.Elem()) - ptr.Elem().Set(o.refValue) - return ptr.Interface(), nil - } - switch typeDesc { - case jsonValueType: - jsonStruct, err := o.ConvertToNative(jsonStructType) - if err != nil { - return nil, err - } - return structpb.NewStructValue(jsonStruct.(*structpb.Struct)), nil - case jsonStructType: - refVal := reflect.Indirect(o.refValue) - refType := refVal.Type() - fields := make(map[string]*structpb.Value, refVal.NumField()) - for i := 0; i < refVal.NumField(); i++ { - fieldType := refType.Field(i) - fieldValue := refVal.Field(i) - if !fieldValue.IsValid() || fieldValue.IsZero() { - continue - } - fieldName := toFieldName(o.valType.fieldNameHandler, fieldType) - fieldCELVal := o.NativeToValue(fieldValue.Interface()) - fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType) - if err != nil { - return nil, err - } - fields[fieldName] = fieldJSONVal.(*structpb.Value) - } - return &structpb.Struct{Fields: fields}, nil - } - return nil, fmt.Errorf("type conversion error from '%v' to '%v'", o.Type(), typeDesc) -} - -// ConvertToType implements the ref.Val interface method. -func (o *nativeObj) ConvertToType(typeVal ref.Type) ref.Val { - switch typeVal { - case types.TypeType: - return o.valType - default: - if typeVal.TypeName() == o.valType.typeName { - return o - } - } - return types.NewErr("type conversion error from '%s' to '%s'", o.Type(), typeVal) -} - -// Equal implements the ref.Val interface method. -// -// Note, that in Golang a pointer to a value is not equal to the value it contains. -// In CEL pointers and values to which they point are equal. -func (o *nativeObj) Equal(other ref.Val) ref.Val { - otherNtv, ok := other.(*nativeObj) - if !ok { - return types.False - } - val := o.val - otherVal := otherNtv.val - refVal := o.refValue - otherRefVal := otherNtv.refValue - if refVal.Kind() != otherRefVal.Kind() { - if refVal.Kind() == reflect.Pointer { - val = refVal.Elem().Interface() - } else if otherRefVal.Kind() == reflect.Pointer { - otherVal = otherRefVal.Elem().Interface() - } - } - return types.Bool(reflect.DeepEqual(val, otherVal)) -} - -// IsZeroValue indicates whether the contained Golang value is a zero value. -// -// Golang largely follows proto3 semantics for zero values. -func (o *nativeObj) IsZeroValue() bool { - return reflect.Indirect(o.refValue).IsZero() -} - -// IsSet tests whether a field which is defined is set to a non-default value. -func (o *nativeObj) IsSet(field ref.Val) ref.Val { - refField, refErr := o.getReflectedField(field) - if refErr != nil { - return refErr - } - return types.Bool(!refField.IsZero()) -} - -// Get returns the value fo a field name. -func (o *nativeObj) Get(field ref.Val) ref.Val { - refField, refErr := o.getReflectedField(field) - if refErr != nil { - return refErr - } - return adaptFieldValue(o, refField) -} - -func (o *nativeObj) getReflectedField(field ref.Val) (reflect.Value, ref.Val) { - fieldName, ok := field.(types.String) - if !ok { - return reflect.Value{}, types.MaybeNoSuchOverloadErr(field) - } - fieldNameStr := string(fieldName) - refField, isDefined := o.valType.hasField(fieldNameStr) - if !isDefined { - return reflect.Value{}, types.NewErr("no such field: %s", fieldName) - } - refVal := reflect.Indirect(o.refValue) - return refVal.FieldByIndex(refField.Index), nil -} - -// Type implements the ref.Val interface method. -func (o *nativeObj) Type() ref.Type { - return o.valType -} - -// Value implements the ref.Val interface method. -func (o *nativeObj) Value() any { - return o.val -} - -func newNativeTypes(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) ([]*nativeType, error) { - nt, err := newNativeType(fieldNameHandler, rawType) - if err != nil { - return nil, err - } - result := []*nativeType{nt} - - alreadySeen := make(map[string]struct{}) - var iterateStructMembers func(reflect.Type) - iterateStructMembers = func(t reflect.Type) { - if t.Implements(reflect.TypeFor[ref.Val]()) { - // skip this field since it's a CEL ref.Val instance. - return - } - if k := t.Kind(); k == reflect.Pointer || k == reflect.Slice || k == reflect.Array || k == reflect.Map { - iterateStructMembers(t.Elem()) - return - } - if t.Kind() != reflect.Struct { - return - } - if _, seen := alreadySeen[t.String()]; seen { - return - } - alreadySeen[t.String()] = struct{}{} - nt, ntErr := newNativeType(fieldNameHandler, t) - if ntErr != nil { - err = ntErr - return - } - result = append(result, nt) - - for idx := 0; idx < t.NumField(); idx++ { - iterateStructMembers(t.Field(idx).Type) - } - } - iterateStructMembers(rawType) - - return result, err -} - -var ( - errDuplicatedFieldName = errors.New("field name already exists in struct") -) - -func newNativeType(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) (*nativeType, error) { - refType := rawType - if refType.Kind() == reflect.Pointer { - refType = refType.Elem() - } - if !isValidObjectType(refType) { - return nil, fmt.Errorf("unsupported reflect.Type %v, must be reflect.Struct", rawType) - } - - // Since naming collisions can only happen with struct tag parsing, we only check for them if it is enabled. - if fieldNameHandler != nil { - fieldNames := make(map[string]struct{}) - - for idx := 0; idx < refType.NumField(); idx++ { - field := refType.Field(idx) - fieldName := toFieldName(fieldNameHandler, field) - - if _, found := fieldNames[fieldName]; found { - return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName) - } else { - fieldNames[fieldName] = struct{}{} - } - } - } - - return &nativeType{ - typeName: fmt.Sprintf("%s.%s", simplePkgAlias(refType.PkgPath()), refType.Name()), - refType: refType, - fieldNameHandler: fieldNameHandler, - }, nil -} - -type nativeType struct { - typeName string - refType reflect.Type - fieldNameHandler NativeTypesFieldNameHandler -} - -// ConvertToNative implements ref.Val.ConvertToNative. -func (t *nativeType) ConvertToNative(typeDesc reflect.Type) (any, error) { - return nil, fmt.Errorf("type conversion error for type to '%v'", typeDesc) -} - -// ConvertToType implements ref.Val.ConvertToType. -func (t *nativeType) ConvertToType(typeVal ref.Type) ref.Val { - switch typeVal { - case types.TypeType: - return types.TypeType - } - return types.NewErr("type conversion error from '%s' to '%s'", types.TypeType, typeVal) -} - -// Equal returns true of both type names are equal to each other. -func (t *nativeType) Equal(other ref.Val) ref.Val { - otherType, ok := other.(ref.Type) - return types.Bool(ok && t.TypeName() == otherType.TypeName()) -} - -// HasTrait implements the ref.Type interface method. -func (t *nativeType) HasTrait(trait int) bool { - return nativeObjTraitMask&trait == trait -} - -// String implements the strings.Stringer interface method. -func (t *nativeType) String() string { - return t.typeName -} - -// Type implements the ref.Val interface method. -func (t *nativeType) Type() ref.Type { - return types.TypeType -} - -// TypeName implements the ref.Type interface method. -func (t *nativeType) TypeName() string { - return t.typeName -} - -// Value implements the ref.Val interface method. -func (t *nativeType) Value() any { - return t.typeName -} - -// fieldByName returns the corresponding reflect.StructField for the give name either by matching -// field tag or field name. -func (t *nativeType) fieldByName(fieldName string) (reflect.StructField, bool) { - if t.fieldNameHandler == nil { - return t.refType.FieldByName(fieldName) - } - - for i := 0; i < t.refType.NumField(); i++ { - f := t.refType.Field(i) - if toFieldName(t.fieldNameHandler, f) == fieldName { - return f, true - } - } - - return reflect.StructField{}, false -} - -// hasField returns whether a field name has a corresponding Golang reflect.StructField -func (t *nativeType) hasField(fieldName string) (reflect.StructField, bool) { - f, found := t.fieldByName(fieldName) - if !found || !f.IsExported() || !isSupportedType(f.Type) { - return reflect.StructField{}, false - } - return f, true -} - -func adaptFieldValue(adapter types.Adapter, refField reflect.Value) ref.Val { - return adapter.NativeToValue(getFieldValue(refField)) -} - -func getFieldValue(refField reflect.Value) any { - if refField.IsZero() { - switch refField.Kind() { - case reflect.Struct: - if refField.Type() == timestampType { - return time.Unix(0, 0) - } - case reflect.Pointer: - return reflect.New(refField.Type().Elem()).Interface() - } - } - return refField.Interface() -} - -func simplePkgAlias(pkgPath string) string { - paths := strings.Split(pkgPath, "/") - if len(paths) == 0 { - return "" - } - return paths[len(paths)-1] -} - -func isValidObjectType(refType reflect.Type) bool { - return refType.Kind() == reflect.Struct -} - -func isSupportedType(refType reflect.Type) bool { - switch refType.Kind() { - case reflect.Chan, reflect.Complex64, reflect.Complex128, reflect.Func, reflect.UnsafePointer, reflect.Uintptr: - return false - case reflect.Array, reflect.Slice: - return isSupportedType(refType.Elem()) - case reflect.Map: - return isSupportedType(refType.Key()) && isSupportedType(refType.Elem()) - } - return true -} - -var ( - pbMsgInterfaceType = reflect.TypeFor[protoreflect.ProtoMessage]() - refValType = reflect.TypeFor[ref.Val]() - timestampType = reflect.TypeFor[time.Time]() - durationType = reflect.TypeFor[time.Duration]() -) diff --git a/vendor/github.com/google/cel-go/interpreter/activation.go b/vendor/github.com/google/cel-go/interpreter/activation.go deleted file mode 100644 index 023efbb8070..00000000000 --- a/vendor/github.com/google/cel-go/interpreter/activation.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package interpreter - -import ( - "errors" - "fmt" - - "github.com/google/cel-go/common/types/ref" -) - -// Activation used to resolve identifiers by name and references by id. -// -// An Activation is the primary mechanism by which a caller supplies input into a CEL program. -type Activation interface { - // ResolveName returns a value from the activation by qualified name, or false if the name - // could not be found. - ResolveName(name string) (any, bool) - - // Parent returns the parent of the current activation, may be nil. - // If non-nil, the parent will be searched during resolve calls. - Parent() Activation -} - -// EmptyActivation returns a variable-free activation. -func EmptyActivation() Activation { - return emptyActivation{} -} - -// emptyActivation is a variable-free activation. -type emptyActivation struct{} - -func (emptyActivation) ResolveName(string) (any, bool) { return nil, false } -func (emptyActivation) Parent() Activation { return nil } - -// NewActivation returns an activation based on a map-based binding where the map keys are -// expected to be qualified names used with ResolveName calls. -// -// The input `bindings` may either be of type `Activation` or `map[string]any`. -// -// Lazy bindings may be supplied within the map-based input in either of the following forms: -// - func() any -// - func() ref.Val -// -// The output of the lazy binding will overwrite the variable reference in the internal map. -// -// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using -// the types.Adapter configured in the environment. -func NewActivation(bindings any) (Activation, error) { - if bindings == nil { - return nil, errors.New("bindings must be non-nil") - } - a, isActivation := bindings.(Activation) - if isActivation { - return a, nil - } - m, isMap := bindings.(map[string]any) - if !isMap { - return nil, fmt.Errorf( - "activation input must be an activation or map[string]interface: got %T", - bindings) - } - return &mapActivation{bindings: m}, nil -} - -// mapActivation which implements Activation and maps of named values. -// -// Named bindings may lazily supply values by providing a function which accepts no arguments and -// produces an interface value. -type mapActivation struct { - bindings map[string]any -} - -// Parent implements the Activation interface method. -func (a *mapActivation) Parent() Activation { - return nil -} - -// ResolveName implements the Activation interface method. -func (a *mapActivation) ResolveName(name string) (any, bool) { - obj, found := a.bindings[name] - if !found { - return nil, false - } - fn, isLazy := obj.(func() ref.Val) - if isLazy { - obj = fn() - a.bindings[name] = obj - } - fnRaw, isLazy := obj.(func() any) - if isLazy { - obj = fnRaw() - a.bindings[name] = obj - } - return obj, found -} - -// hierarchicalActivation which implements Activation and contains a parent and -// child activation. -type hierarchicalActivation struct { - parent Activation - child Activation - poolAllocated bool -} - -// Parent implements the Activation interface method. -func (a *hierarchicalActivation) Parent() Activation { - return a.parent -} - -// ResolveName implements the Activation interface method. -func (a *hierarchicalActivation) ResolveName(name string) (any, bool) { - if object, found := a.child.ResolveName(name); found { - return object, found - } - return a.parent.ResolveName(name) -} - -// 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: parent, child: child, poolAllocated: false} -} - -// NewPartialActivation returns an Activation which contains a list of AttributePattern values -// representing field and index operations that should result in a 'types.Unknown' result. -// -// The `bindings` value may be any value type supported by the interpreter.NewActivation call, -// but is typically either an existing Activation or map[string]any. -func NewPartialActivation(bindings any, - unknowns ...*AttributePattern) (PartialActivation, error) { - a, err := NewActivation(bindings) - if err != nil { - return nil, err - } - return &partActivation{Activation: a, unknowns: unknowns}, nil -} - -// PartialActivation extends the Activation interface with a set of UnknownAttributePatterns. -type PartialActivation interface { - Activation - - // UnknownAttributePaths returns a set of AttributePattern values which match Attribute - // expressions for data accesses whose values are not yet known. - UnknownAttributePatterns() []*AttributePattern -} - -// partialActivationConverter indicates whether an Activation implementation supports conversion to a PartialActivation -type partialActivationConverter interface { - // AsPartialActivation converts the current activation to a PartialActivation - AsPartialActivation() (PartialActivation, bool) -} - -// partActivation is the default implementations of the PartialActivation interface. -type partActivation struct { - Activation - unknowns []*AttributePattern -} - -// UnknownAttributePatterns implements the PartialActivation interface method. -func (a *partActivation) UnknownAttributePatterns() []*AttributePattern { - return a.unknowns -} - -// AsPartialActivation returns the partActivation as a PartialActivation interface. -func (a *partActivation) AsPartialActivation() (PartialActivation, bool) { - return a, true -} - -// AsPartialActivation walks the activation hierarchy and returns the first PartialActivation, if found. -func AsPartialActivation(vars Activation) (PartialActivation, bool) { - // Only internal activation instances may implement this interface - if pv, ok := vars.(partialActivationConverter); ok { - return pv.AsPartialActivation() - } - // Since Activations may be hierarchical, test whether a parent converts to a PartialActivation - if vars.Parent() != nil { - return AsPartialActivation(vars.Parent()) - } - return nil, false -} diff --git a/vendor/github.com/google/cel-go/interpreter/frame.go b/vendor/github.com/google/cel-go/interpreter/frame.go deleted file mode 100644 index 3cf2d6b6b5b..00000000000 --- a/vendor/github.com/google/cel-go/interpreter/frame.go +++ /dev/null @@ -1,327 +0,0 @@ -// 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() -) diff --git a/vendor/github.com/google/cel-go/interpreter/runtimecost.go b/vendor/github.com/google/cel-go/interpreter/runtimecost.go deleted file mode 100644 index 68e4310106b..00000000000 --- a/vendor/github.com/google/cel-go/interpreter/runtimecost.go +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2022 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package interpreter - -import ( - "errors" - "math" - - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/overloads" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" -) - -// WARNING: Any changes to cost calculations in this file require a corresponding change in checker/cost.go - -// ActualCostEstimator provides function call cost estimations at runtime -// CallCost returns an estimated cost for the function overload invocation with the given args, or nil if it has no -// estimate to provide. CEL attempts to provide reasonable estimates for its standard function library, so CallCost -// should typically not need to provide an estimate for CELs standard function. -type ActualCostEstimator interface { - CallCost(function, overloadID string, args []ref.Val, result ref.Val) *uint64 -} - -// costTrackPlanOption modifies the cost tracking factory associatied with the CostObserver -type costTrackPlanOption func(*costTrackerFactory) *costTrackerFactory - -// CostTrackerFactory configures the factory method to generate a new cost-tracker per-evaluation. -func CostTrackerFactory(factory func() (*CostTracker, error)) costTrackPlanOption { - return func(fac *costTrackerFactory) *costTrackerFactory { - fac.factory = factory - return fac - } -} - -// CostObserver provides an observer that tracks runtime cost. -func CostObserver(opts ...costTrackPlanOption) PlannerOption { - ct := &costTrackerFactory{} - for _, o := range opts { - ct = o(ct) - } - return func(p *planner) (*planner, error) { - if ct.factory == nil { - return nil, errors.New("cost tracker factory not configured") - } - p.observers = append(p.observers, ct) - p.decorators = append(p.decorators, decObserveEval(ct.Observe)) - return p, nil - } -} - -// costTrackerFactory holds a factory for producing new CostTracker instances on each Eval call. -type costTrackerFactory struct { - factory func() (*CostTracker, error) -} - -// InitState produces a CostTracker and bundles it into an Activation in a way which is not visible -// to expression evaluation. -func (ct *costTrackerFactory) InitState(frame *ExecutionFrame) (any, error) { - tracker, err := ct.factory() - if err != nil { - return nil, err - } - 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(frame *ExecutionFrame) any { - if frame == nil || frame.ctx == 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) { - 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) { - case ConstantQualifier: - // TODO: Push identifiers on to the stack before observing constant qualifiers that apply to them - // and enable the below pop. Once enabled this can case can be collapsed into the Qualifier case. - tracker.cost++ - case InterpretableConst: - // zero cost - case InterpretableAttribute: - switch a := t.Attr().(type) { - case *conditionalAttribute: - // Ternary has no direct cost. All cost is from the conditional and the true/false branch expressions. - tracker.stack.drop(a.falsy.ID(), a.truthy.ID(), a.expr.ID()) - default: - tracker.stack.drop(t.Attr().ID()) - tracker.cost += common.SelectAndIdentCost - } - if !tracker.presenceTestHasCost { - if _, isTestOnly := programStep.(*evalTestOnly); isTestOnly { - tracker.cost -= common.SelectAndIdentCost - } - } - case *evalExhaustiveConditional: - // Ternary has no direct cost. All cost is from the conditional and the true/false branch expressions. - tracker.stack.drop(t.attr.falsy.ID(), t.attr.truthy.ID(), t.attr.expr.ID()) - - // While the field names are identical, the boolean operation eval structs do not share an interface and so - // must be handled individually. - case *evalOr: - for _, term := range t.terms { - tracker.stack.drop(term.ID()) - } - case *evalAnd: - for _, term := range t.terms { - tracker.stack.drop(term.ID()) - } - case *evalExhaustiveOr: - for _, term := range t.terms { - tracker.stack.drop(term.ID()) - } - case *evalExhaustiveAnd: - for _, term := range t.terms { - tracker.stack.drop(term.ID()) - } - case *evalFold: - tracker.stack.drop(t.iterRange.ID()) - case Qualifier: - tracker.cost++ - case InterpretableCall: - if argVals, ok := tracker.stack.dropArgs(t.Args()); ok { - tracker.cost += tracker.costCall(t, argVals, val) - } - case InterpretableConstructor: - tracker.stack.dropArgs(t.InitVals()) - switch t.Type() { - case types.ListType: - tracker.cost += common.ListCreateBaseCost - case types.MapType: - tracker.cost += common.MapCreateBaseCost - default: - tracker.cost += common.StructCreateBaseCost - } - } - tracker.stack.push(val, id) - - if tracker.Limit != nil && tracker.cost > *tracker.Limit { - panic(EvalCancelledError{Cause: CostLimitExceeded, Message: "operation cancelled: actual cost limit exceeded"}) - } -} - -// CostTrackerOption configures the behavior of CostTracker objects. -type CostTrackerOption func(*CostTracker) error - -// CostTrackerLimit sets the runtime limit on the evaluation cost during execution and will terminate the expression -// evaluation if the limit is exceeded. -func CostTrackerLimit(limit uint64) CostTrackerOption { - return func(tracker *CostTracker) error { - tracker.Limit = &limit - return nil - } -} - -// PresenceTestHasCost determines whether presence testing has a cost of one or zero. -// Defaults to presence test has a cost of one. -func PresenceTestHasCost(hasCost bool) CostTrackerOption { - return func(tracker *CostTracker) error { - tracker.presenceTestHasCost = hasCost - return nil - } -} - -// NewCostTracker creates a new CostTracker with a given estimator and a set of functional CostTrackerOption values. -func NewCostTracker(estimator ActualCostEstimator, opts ...CostTrackerOption) (*CostTracker, error) { - tracker := &CostTracker{ - Estimator: estimator, - overloadTrackers: map[string]FunctionTracker{}, - presenceTestHasCost: true, - } - for _, opt := range opts { - err := opt(tracker) - if err != nil { - return nil, err - } - } - return tracker, nil -} - -// OverloadCostTracker binds an overload ID to a runtime FunctionTracker implementation. -// -// OverloadCostTracker instances augment or override ActualCostEstimator decisions, allowing for versioned and/or -// optional cost tracking changes. -func OverloadCostTracker(overloadID string, fnTracker FunctionTracker) CostTrackerOption { - return func(tracker *CostTracker) error { - tracker.overloadTrackers[overloadID] = fnTracker - return nil - } -} - -// FunctionTracker computes the actual cost of evaluating the functions with the given arguments and result. -type FunctionTracker func(args []ref.Val, result ref.Val) *uint64 - -// CostTracker represents the information needed for tracking runtime cost. -type CostTracker struct { - Estimator ActualCostEstimator - overloadTrackers map[string]FunctionTracker - Limit *uint64 - presenceTestHasCost bool - - cost uint64 - stack refValStack -} - -// 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 -} - -func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result ref.Val) uint64 { - var cost uint64 - if len(c.overloadTrackers) != 0 { - if tracker, found := c.overloadTrackers[call.OverloadID()]; found { - callCost := tracker(args, result) - if callCost != nil { - cost += *callCost - return cost - } - } - } - if c.Estimator != nil { - callCost := c.Estimator.CallCost(call.Function(), call.OverloadID(), args, result) - if callCost != nil { - cost += *callCost - return cost - } - } - // if user didn't specify, the default way of calculating runtime cost would be used. - // if user has their own implementation of ActualCostEstimator, make sure to cover the mapping between overloadId and cost calculation - switch call.OverloadID() { - // O(n) functions - case overloads.StartsWithString, overloads.EndsWithString: - 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. - // We just assume all list containment checks are O(n). - cost += actualSize(args[1]) - // O(min(m, n)) functions - case overloads.LessString, overloads.GreaterString, overloads.LessEqualsString, overloads.GreaterEqualsString, - overloads.LessBytes, overloads.GreaterBytes, overloads.LessEqualsBytes, overloads.GreaterEqualsBytes, - overloads.Equals, overloads.NotEquals: - // When we check the equality of 2 scalar values (e.g. 2 integers, 2 floating-point numbers, 2 booleans etc.), - // the CostTracker.ActualSize() function by definition returns 1 for each operand, resulting in an overall cost - // of 1. - lhsSize := actualSize(args[0]) - rhsSize := actualSize(args[1]) - minSize := lhsSize - if rhsSize < minSize { - minSize = rhsSize - } - cost += uint64(math.Ceil(float64(minSize) * common.StringTraversalCostFactor)) - // O(m+n) functions - case overloads.AddString, overloads.AddBytes: - // In the worst case scenario, we would need to reallocate a new backing store and copy both operands over. - cost += uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1])) * common.StringTraversalCostFactor)) - // O(nm) functions - 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. - strCost := uint64(math.Ceil((1.0 + float64(actualSize(args[0]))) * common.StringTraversalCostFactor)) - // We don't know how many expressions are in the regex, just the string length (a huge - // improvement here would be to somehow get a count the number of expressions in the regex or - // how many states are in the regex state machine and use that to measure regex cost). - // For now, we're making a guess that each expression in a regex is typically at least 4 chars - // in length. - regexCost := uint64(math.Ceil(float64(actualSize(args[1])) * common.RegexStringLengthCostFactor)) - cost += strCost * regexCost - case overloads.ContainsString: - strCost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) - substrCost := uint64(math.Ceil(float64(actualSize(args[1])) * common.StringTraversalCostFactor)) - cost += strCost * substrCost - - default: - // The following operations are assumed to have O(1) complexity. - // - AddList due to the implementation. Index lookup can be O(c) the - // number of concatenated lists, but we don't track that is cost calculations. - // - Conversions, since none perform a traversal of a type of unbound length. - // - Computing the size of strings, byte sequences, lists and maps. - // - Logical operations and all operators on fixed width scalars (comparisons, equality) - // - Any functions that don't have a declared cost either here or in provided ActualCostEstimator. - cost++ - - } - return cost -} - -// actualSize returns the size of the value for all traits.Sizer values, a fixed size for all proto-based -// objects, and a size of 1 for all other value types. -func actualSize(value ref.Val) uint64 { - if sz, ok := value.(traits.Sizer); ok { - return uint64(sz.Size().(types.Int)) - } - if opt, ok := value.(*types.Optional); ok && opt.HasValue() { - return actualSize(opt.GetValue()) - } - return 1 -} - -type stackVal struct { - Val ref.Val - ID int64 -} - -// refValStack keeps track of values of the stack for cost calculation purposes -type refValStack []stackVal - -func (s *refValStack) push(val ref.Val, id int64) { - value := stackVal{Val: val, ID: id} - *s = append(*s, value) -} - -// TODO: Allowing drop and dropArgs to remove stack items above the IDs they are provided is a workaround. drop and dropArgs -// should find and remove only the stack items matching the provided IDs once all attributes are properly pushed and popped from stack. - -// drop searches the stack for each ID and removes the ID and all stack items above it. -// If none of the IDs are found, the stack is not modified. -// WARNING: It is possible for multiple expressions with the same ID to exist (due to how macros are implemented) so it's -// possible that a dropped ID will remain on the stack. They should be removed when IDs on the stack are popped. -func (s *refValStack) drop(ids ...int64) { - for _, id := range ids { - for idx := len(*s) - 1; idx >= 0; idx-- { - if (*s)[idx].ID == id { - *s = (*s)[:idx] - break - } - } - } -} - -// dropArgs searches the stack for all the args by their IDs, accumulates their associated ref.Vals and drops any -// stack items above any of the arg IDs. If any of the IDs are not found the stack, false is returned. -// Args are assumed to be found in the stack in reverse order, i.e. the last arg is expected to be found highest in -// the stack. -// WARNING: It is possible for multiple expressions with the same ID to exist (due to how macros are implemented) so it's -// possible that a dropped ID will remain on the stack. They should be removed when IDs on the stack are popped. -func (s *refValStack) dropArgs(args []InterpretableV2) ([]ref.Val, bool) { - result := make([]ref.Val, len(args)) -argloop: - for nIdx := len(args) - 1; nIdx >= 0; nIdx-- { - for idx := len(*s) - 1; idx >= 0; idx-- { - if (*s)[idx].ID == args[nIdx].ID() { - el := (*s)[idx] - *s = (*s)[:idx] - result[nIdx] = el.Val - continue argloop - } - } - return nil, false - } - return result, true -} diff --git a/vendor/modules.txt b/vendor/modules.txt index ae39c1cf162..3ac8ce9202a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,6 +2,33 @@ # bitbucket.org/bertimus9/systemstat v0.5.0 ## explicit; go 1.17 bitbucket.org/bertimus9/systemstat +# cel.dev/cel-go v0.33.0 +## explicit; go 1.23.0 +cel.dev/cel-go/cel +cel.dev/cel-go/cel/async +cel.dev/cel-go/checker +cel.dev/cel-go/checker/decls +cel.dev/cel-go/common +cel.dev/cel-go/common/ast +cel.dev/cel-go/common/containers +cel.dev/cel-go/common/cost +cel.dev/cel-go/common/debug +cel.dev/cel-go/common/decls +cel.dev/cel-go/common/env +cel.dev/cel-go/common/functions +cel.dev/cel-go/common/operators +cel.dev/cel-go/common/overloads +cel.dev/cel-go/common/runes +cel.dev/cel-go/common/stdlib +cel.dev/cel-go/common/types +cel.dev/cel-go/common/types/pb +cel.dev/cel-go/common/types/ref +cel.dev/cel-go/common/types/traits +cel.dev/cel-go/ext +cel.dev/cel-go/interpreter +cel.dev/cel-go/interpreter/functions +cel.dev/cel-go/parser +cel.dev/cel-go/parser/gen # cel.dev/expr v0.25.3 ## explicit; go 1.23.0 cel.dev/expr @@ -280,31 +307,6 @@ github.com/google/cadvisor/lib/utils/sysfs github.com/google/cadvisor/lib/utils/sysinfo github.com/google/cadvisor/lib/version github.com/google/cadvisor/lib/watcher -# github.com/google/cel-go v0.29.2 -## explicit; go 1.23.0 -github.com/google/cel-go/cel -github.com/google/cel-go/checker -github.com/google/cel-go/checker/decls -github.com/google/cel-go/common -github.com/google/cel-go/common/ast -github.com/google/cel-go/common/containers -github.com/google/cel-go/common/debug -github.com/google/cel-go/common/decls -github.com/google/cel-go/common/env -github.com/google/cel-go/common/functions -github.com/google/cel-go/common/operators -github.com/google/cel-go/common/overloads -github.com/google/cel-go/common/runes -github.com/google/cel-go/common/stdlib -github.com/google/cel-go/common/types -github.com/google/cel-go/common/types/pb -github.com/google/cel-go/common/types/ref -github.com/google/cel-go/common/types/traits -github.com/google/cel-go/ext -github.com/google/cel-go/interpreter -github.com/google/cel-go/interpreter/functions -github.com/google/cel-go/parser -github.com/google/cel-go/parser/gen # github.com/google/gnostic-models v0.7.1 ## explicit; go 1.22 github.com/google/gnostic-models/compiler