Skip to content
Merged
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
16 changes: 12 additions & 4 deletions checker/cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,18 +791,26 @@ func (c *coster) functionCost(e ast.Expr, function, overloadID string, target *A
return CallEstimate{CostEstimate: c.sizeOrUnknown(args[1]).MultiplyByCostFactor(1).Add(argCostSum())}
}
// O(nm) functions
case overloads.MatchesString:
case overloads.Matches, overloads.MatchesString:
// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
if target != nil && len(args) == 1 {
var strNode, regexNode AstNode
if overloadID == overloads.MatchesString && target != nil && len(args) == 1 {
strNode = *target
regexNode = args[0]
} else if overloadID == overloads.Matches && target == nil && len(args) == 2 {
strNode = args[0]
regexNode = args[1]
}
if strNode != nil && regexNode != nil {
// Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0
// in case where string is empty but regex is still expensive.
strCost := c.sizeOrUnknown(*target).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor)
strCost := c.sizeOrUnknown(strNode).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor)
// We don't know how many expressions are in the regex, just the string length (a huge
// improvement here would be to somehow get a count the number of expressions in the regex or
// how many states are in the regex state machine and use that to measure regex cost).
// For now, we're making a guess that each expression in a regex is typically at least 4 chars
// in length.
regexCost := c.sizeOrUnknown(args[0]).MultiplyByCostFactor(common.RegexStringLengthCostFactor)
regexCost := c.sizeOrUnknown(regexNode).MultiplyByCostFactor(common.RegexStringLengthCostFactor)
return CallEstimate{CostEstimate: strCost.Multiply(regexCost).Add(argCostSum())}
}
case overloads.ContainsString:
Expand Down
9 changes: 9 additions & 0 deletions checker/cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ func TestCost(t *testing.T) {
hints: map[string]uint64{"input": 500},
wanted: CostEstimate{Min: 3, Max: 103},
},
{
name: "matches global",
expr: `matches(input, '\\d+a\\d+b')`,
vars: []*decls.VariableDecl{
decls.NewVariable("input", types.StringType),
},
hints: map[string]uint64{"input": 500},
wanted: CostEstimate{Min: 3, Max: 103},
},
{
name: "startsWith",
expr: `input.startsWith(arg1)`,
Expand Down
2 changes: 1 addition & 1 deletion interpreter/runtimecost.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re
// In the worst case scenario, we would need to reallocate a new backing store and copy both operands over.
cost += uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1])) * common.StringTraversalCostFactor))
// O(nm) functions
case overloads.MatchesString:
case overloads.Matches, overloads.MatchesString:
// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
// Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0
// in case where string is empty but regex is still expensive.
Expand Down
9 changes: 9 additions & 0 deletions interpreter/runtimecost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,15 @@ func TestRuntimeCost(t *testing.T) {
want: 103,
in: map[string]any{"input": string(randSeq(500)), "arg1": string(randSeq(500))},
},
{
name: "matches global",
expr: `matches(input, '\\d+a\\d+b')`,
vars: []*decls.VariableDecl{
decls.NewVariable("input", types.StringType),
},
want: 103,
in: map[string]any{"input": string(randSeq(500))},
},
{
name: "startsWith",
expr: `input.startsWith(arg1)`,
Expand Down