package billingexpr import ( "errors" "math" "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/parser" ) // DetectionCostBound proves a conservative cost ceiling rather than sampling // a few token counts. Both conditional branches are bounded, including prices // selected by time/request rules. Unsupported expressions fail closed. func DetectionCostBound(expression string, input, output float64) (float64, error) { _, body := ParseExprVersion(expression) tree, err := parser.Parse(body) if err != nil { return 0, err } var bound func(ast.Node) (float64, error) unsupported := errors.New("detection pricing cannot be safely bounded") bound = func(node ast.Node) (float64, error) { switch n := node.(type) { case *ast.IntegerNode: if n.Value >= 0 { return float64(n.Value), nil } case *ast.FloatNode: if n.Value >= 0 { return n.Value, nil } case *ast.IdentifierNode: switch n.Value { case "p", "len", "cr", "cc", "cc1h": return input, nil case "c": return output, nil case "img", "img_cr", "img_o", "ai", "ao": return 0, nil case "image_count": return 1, nil } case *ast.BinaryNode: a, e := bound(n.Left) if e != nil { return 0, e } b, e := bound(n.Right) if e != nil { return 0, e } switch n.Operator { case "+": return a + b, nil case "*": return a * b, nil case "/": switch n.Right.(type) { case *ast.IntegerNode, *ast.FloatNode: if b > 0 { return a / b, nil } } } case *ast.ConditionalNode: a, e := bound(n.Exp1) if e != nil { return 0, e } b, e := bound(n.Exp2) return max(a, b), e case *ast.CallNode: name, ok := n.Callee.(*ast.IdentifierNode) if !ok { return 0, unsupported } if name.Value == "tier" && len(n.Arguments) == 2 { return bound(n.Arguments[1]) } if name.Value == "fixed" && len(n.Arguments) == 1 { v, e := bound(n.Arguments[0]) return v * 1_000_000, e } if name.Value == "ceil" && len(n.Arguments) == 1 { v, e := bound(n.Arguments[0]) return math.Ceil(v), e } if name.Value == "floor" && len(n.Arguments) == 1 { v, e := bound(n.Arguments[0]) return math.Floor(v), e } if name.Value == "min" || name.Value == "max" { v := float64(0) for _, arg := range n.Arguments { x, e := bound(arg) if e != nil { return 0, e } v = max(v, x) } return v, nil } } return 0, unsupported } value, err := bound(tree.Node) if math.IsNaN(value) || math.IsInf(value, 0) { return 0, unsupported } return value, err }