Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions common/cost/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ go_library(
"default_strategy.go",
"estimator.go",
"model.go",
"standard.go",
"strategy.go",
"tracker.go",
],
Expand All @@ -36,24 +37,21 @@ go_test(
"default_strategy_test.go",
"estimator_test.go",
"model_test.go",
"standard_test.go",
"tracker_test.go",
],
embed = [
":go_default_library",
],
deps = [
"//checker:go_default_library",
"//common:go_default_library",
"//cel:go_default_library",
"//common/ast:go_default_library",
"//common/containers:go_default_library",
"//common/decls:go_default_library",
"//common/operators:go_default_library",
"//common/overloads:go_default_library",
"//common/stdlib:go_default_library",
"//common/types:go_default_library",
"//common/types/ref:go_default_library",
"//interpreter:go_default_library",
"//parser:go_default_library",
"//ext:go_default_library",
"//test/proto3pb:go_default_library",
],
)
16 changes: 14 additions & 2 deletions common/cost/aggregate_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ import (

// 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. By default, stringUnitLength is configured to 1
// so that unscaled character/byte lengths are preserved for cost modeling.
// 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
Expand Down
10 changes: 5 additions & 5 deletions common/cost/aggregate_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ func TestAggregateSizingStrategy_EstimateSize_List(t *testing.T) {
}

tests := []struct {
name string
ctx EstimateContext
node AstNode
wantOk bool
check func(t *testing.T, sz SizeEstimate)
name string
ctx EstimateContext
node AstNode
wantOk bool
check func(t *testing.T, sz SizeEstimate)
}{
{
name: "literal_list_elements",
Expand Down
33 changes: 29 additions & 4 deletions common/cost/cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ func SafeAdd(x, y uint64, rest ...uint64) uint64 {
return sum
}

// SafeSubtract returns the difference of x - y, saturating at zero.
func SafeSubtract(x, y uint64) uint64 {
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 {
Expand Down Expand Up @@ -153,8 +161,8 @@ func RangedSizeEstimate(min, max uint64) SizeEstimate {
return SizeEstimate{Min: min, Max: max}
}

// AtLeastOne returns a size estimate with min and max guaranteed to be at least 1.
func AtLeastOne(size SizeEstimate) SizeEstimate {
// 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
}
Expand Down Expand Up @@ -189,6 +197,17 @@ func (se SizeEstimate) Add(sizeEstimate SizeEstimate) SizeEstimate {
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.
func (se SizeEstimate) Multiply(sizeEstimate SizeEstimate) SizeEstimate {
Expand Down Expand Up @@ -326,6 +345,9 @@ func ActualSize(value ref.Val) uint64 {

// 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
}
Expand Down Expand Up @@ -359,16 +381,19 @@ func EstimateListAlloc(sz SizeEstimate, costFactor float64) (CostEstimate, *Size
// 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 node.Expr().Kind() != ast.LiteralKind {
if node == nil || node.Expr() == nil || node.Expr().Kind() != ast.LiteralKind {
return defaultVal
}
lit := node.Expr().AsLiteral()
if lit.Type() == types.UintType {
return uint64(lit.(types.Uint))
}
if lit.Type() != types.IntType {
return defaultVal
}
val := lit.(types.Int)
if val < types.IntZero {
return 0
}
return uint64(lit.(types.Int))
return uint64(val)
}
112 changes: 111 additions & 1 deletion common/cost/cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,32 @@ package cost
import (
"math"
"testing"

"cel.dev/cel-go/common/ast"
"cel.dev/cel-go/common/types"
)

func TestSafeSubtract(t *testing.T) {
tests := []struct {
name string
x, y uint64
want uint64
}{
{name: "zero", x: 0, y: 0, want: 0},
{name: "simple", x: 5, y: 3, want: 2},
{name: "underflow to zero", x: 3, y: 5, want: 0},
{name: "max minus zero", x: math.MaxUint64, y: 0, want: math.MaxUint64},
{name: "max minus max", x: math.MaxUint64, y: math.MaxUint64, want: 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := SafeSubtract(tc.x, tc.y); got != tc.want {
t.Errorf("SafeSubtract(%d, %d) got %d, want %d", tc.x, tc.y, got, tc.want)
}
})
}
}

func TestSafeAdd(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -144,7 +168,7 @@ func TestSizeEstimate(t *testing.T) {
if got := RangedSizeEstimate(3, 8); got.Min != 3 || got.Max != 8 {
t.Errorf("RangedSizeEstimate(3, 8) = %v, want {3, 8}", got)
}
if got := AtLeastOne(FixedSizeEstimate(0)); got.Min != 1 || got.Max != 1 {
if got := AtLeastOneSize(FixedSizeEstimate(0)); got.Min != 1 || got.Max != 1 {
t.Errorf("AtLeastOne(0) = %v, want {1, 1}", got)
}
}
Expand Down Expand Up @@ -192,3 +216,89 @@ func TestExtCostHelpers(t *testing.T) {
t.Errorf("NewCallEstimate = %v, want CostEstimate=%v ResultSize=%v", callEst, costEst, resSz)
}
}

func TestSizeEstimate_Subtract(t *testing.T) {
s1 := RangedSizeEstimate(5, 15)
s2 := RangedSizeEstimate(2, 4)
got := s1.Subtract(s2)
if got.Min != 1 || got.Max != 13 {
t.Errorf("s1.Subtract(s2) = %v, want {1, 13}", got)
}

// Underflow cases
s3 := RangedSizeEstimate(2, 4)
s4 := RangedSizeEstimate(5, 10)
got2 := s3.Subtract(s4)
if got2.Min != 0 || got2.Max != 0 {
t.Errorf("s3.Subtract(s4) = %v, want {0, 0}", got2)
}
}

func TestEstimateSize(t *testing.T) {
// Nil node
if sz := EstimateSize(nil, nil); sz != UnknownSizeEstimate() {
t.Errorf("EstimateSize(nil, nil) = %v, want unknown", sz)
}

// Node with computed size
compSz := FixedSizeEstimate(42)
nodeWithComp := NewAstNode(nil, nil, types.IntType, &compSz)
if sz := EstimateSize(nil, nodeWithComp); sz != compSz {
t.Errorf("EstimateSize(comp) = %v, want %v", sz, compSz)
}

// Node with estimator
nodeWithoutComp := NewAstNode(nil, []string{"foo"}, types.IntType, nil)
est := testHintsEstimator{hints: map[string]uint64{"foo": 100}}
if sz := EstimateSize(est, nodeWithoutComp); sz != FixedSizeEstimate(100) {
t.Errorf("EstimateSize(est) = %v, want 100", sz)
}

// Node with estimator returning nil
if sz := EstimateSize(est, NewAstNode(nil, []string{"bar"}, types.IntType, nil)); sz != UnknownSizeEstimate() {
t.Errorf("EstimateSize(unknown) = %v, want unknown", sz)
}
}

func TestNodeAsUintValue(t *testing.T) {
// Nil node
if val := NodeAsUintValue(nil, 99); val != 99 {
t.Errorf("NodeAsUintValue(nil) = %d, want 99", val)
}

// Non-literal node
fac := ast.NewExprFactory()
identExpr := fac.NewIdent(1, "x")
identNode := NewAstNode(identExpr, nil, types.IntType, nil)
if val := NodeAsUintValue(identNode, 99); val != 99 {
t.Errorf("NodeAsUintValue(ident) = %d, want 99", val)
}

// Non-int literal (string)
strExpr := fac.NewLiteral(2, types.String("hello"))
strNode := NewAstNode(strExpr, nil, types.StringType, nil)
if val := NodeAsUintValue(strNode, 99); val != 99 {
t.Errorf("NodeAsUintValue(str) = %d, want 99", val)
}

// Positive int literal
intExpr := fac.NewLiteral(3, types.Int(42))
intNode := NewAstNode(intExpr, nil, types.IntType, nil)
if val := NodeAsUintValue(intNode, 99); val != 42 {
t.Errorf("NodeAsUintValue(int 42) = %d, want 42", val)
}

// Negative int literal (saturates at 0)
negExpr := fac.NewLiteral(4, types.Int(-5))
negNode := NewAstNode(negExpr, nil, types.IntType, nil)
if val := NodeAsUintValue(negNode, 99); val != 0 {
t.Errorf("NodeAsUintValue(int -5) = %d, want 0", val)
}

// Uint literal
uintExpr := fac.NewLiteral(5, types.Uint(100))
uintNode := NewAstNode(uintExpr, nil, types.UintType, nil)
if val := NodeAsUintValue(uintNode, 99); val != 100 {
t.Errorf("NodeAsUintValue(uint 100) = %d, want 100", val)
}
}
Loading
Loading