-
Notifications
You must be signed in to change notification settings - Fork 231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Late Function Bindings in Go Cel Evaluation #356
Comments
We're discussing a generalization of this - allow functions to access the full Activation, not just the supplied variables. This way you could bind |
I have stumbled upon a use case for this feature which would be elegantly solved by allowing to override some of the bindings used during program evaluation. By digging down into the source code the function bindings are resolved when the program is created here: https://github.com/google/cel-go/blob/master/cel/program.go#L180-L203 and become part of the interpreter that is used to create the interpretable. Out of function calls found in the expression, the planner will create one of the following Interpretable implementations (reference: https://github.com/google/cel-go/blob/master/interpreter/planner.go#L211-L285):
because all of them retain a I think we should still be checking during Another option would be passing a decorator to the I don't think that the code changes are massive, they seem rather contained to a couple of files at first site. If this is a viable solution I can prepare a PR that implement one of the two approaches below: Approach 1: With Activation and Internal CustomDecorator
Approach 2: With Custom Decorator Only
Approach 3: Add a ProgramOptionUnfortunately, we cannot achieve this by adding an additional VerdictPersonally I think that implementing a custom InterpretableDecorator for this task and defining a new Activation type including bindings might be a better way, simply because we don't have to modify the interface of the types and therefore we can use this behaviour with the existing code. Therefore Approach 1 would be preferred, also because with respect to Approach 2 it does not open up to other behaviours that we don't want to support yet. Suggested Code Changesin: https://github.com/google/cel-go/blob/master/interpreter/decorators.go // LateBindinsDispatcher returns a dispatcher that contains the supplied overloads.
// The function checks that each of the overload is defined in the parent dispatcher
// because they are expected to be overrides and not new dispatchers. If any of the
// dispatchers does not match a corresponding overload in the parent then or it has
// a different arity, it then returns an error.
func lateBindingsDispatcher(bindings []*functions.Overload, parent Dispatcher) (Dispatcher, error) {
lateDispatcher := &defaultDispatcher{
overloads: make(map[string]*functions.Overload)
}
for _, binding := range bindings {
// does the current dispatcher have the overload?
// if not skip and don't include it
if overload, found := dispatcher.FindOverload(binding.Operator); found {
// the lightest check we can do is ensuring at least
// that the number of parameters matches, by checking
// which operator is defined. We don't have the ability
// to check that types are compatible because there is
// not enough information to do this check.
if (binding.UnaryOp == nil && overload.UnaryOp != nil) || (binding.UnaryOp != nil && binding.UnaryOp == nil) {
return nil, fmt.Errorf("overload unary operator mismatch (id: '%s')", binding.Operator)
}
if (binding.BinaryOp == nil && binding.BinaryOp != nil) || (binding.BinaryOp != nil && binding.BinaryOp == nil) {
return nil, fmt.Errorf("overload binary operator mismtach (id: '%s')", binding.Operator)
}
if (binding.FunctionOp == nil && binding.FunctionOp != nil) || (binding.FunctionOp != nil && binding.FunctionOp == nil) {
return nil, fmt.Errorf("overload function operator mismtach (id: '%s')", binding.Operator)
}
lateDispatcher.overloads[binding.Operator] = binding
} else {
return nil, fmt.Errorf("overload does not exist: '%s'", binding.Operator)
}
}
return &lateDispatcher, nil
}
// lateBindingDispatcher produces an interpretable decorator that replaces
// function calls with the overloads defined in the dispatcher passed as
// arguments. If the overload is not found, no change is made to the
// interpretable.
func lateBindingsDecorator(dispatcher Dispatcher) InterpretableDecorator {
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
call, ok := i.(interpreter.InterpretableCall)
if !ok {
return i, nil
}
switch inst := i.(type) {
case *evalZeroArity:
overload, found := dispatcher.FindOverload(inst.overload)
if found {
return &evalZeroArity{
id: inst.id,
function: inst.function,
overload: inst.overload,
impl: overload.FunctionOp,
}, nil
}
case *evalUnary:
overload, found := dispatcher.FindOverload(inst.overload)
if found {
return &evalUnary{
id: inst.id,
interpretable: inst.interpretable,
function: inst.function,
overload: inst.overload,
trait: inst.trait,
nonStrict: inst.nonStrict,
impl: overload.UnaryOp,
}, nil
}
case *evalBinary:
overload, found := dispatcher.FindOverload(inst.overload)
if found {
return &evalBinary{
id: inst.id,
lhs: inst.lhs,
rhs: inst.rhs,
function: inst.function,
overload: inst.overload,
trait: inst.trait,
nonStrict: inst.nonStrict,
impl: overload.UnaryOp,
}, nil
}
case *evalVarArgs:
verload, found := dispatcher.FindOverload(inst.overload)
if found {
return &evalVarArgs{
id: inst.id,
args: inst.args,
function: inst.function,
overload: inst.overload,
trait: inst.trait,
nonStrict: inst.nonStrict,
impl: overload.UnaryOp,
}, nil
}
default:
return nil, types.NewErr("invalid type: %s", inst)
}
return i, nil
}
}
func ReplaceBindings(
interpretable intepreter.Interpretable,
bindings []*function.Overload,
dispatcher Dispatcher
) (interpreter.Interpretable, error) {
lateDispatcher, err := interpreter.LateBindingDispatcher(bindings, p.dispatcher)
if err != nil {
return nil, err
}
lateDecorator = lateBindingsDecorator(lateDispatcher)
// here is where we need to have the ability to visit the interpretable
// the decorator I have implemented only performs the replacement
// if the input is of type InterpretableCall.
return visit(interpretable, lateDecorator)
} in: https://github.com/google/cel-go/blob/master/interpreter/activation.go // Overloads returns the function overloads that have been added to the
// activation. The function recursively check the activations based on
// the known types.
func Overloads(activation interpreter.Activation) map[string]*functions.Overload {
overloads := map[string]*functions.Overload{}
if activation == nil {
return overloads
}
switch inst := activation.(type) {
case *emptyActivation:
return []*functions.Overload{},
case *mapActivation:
for k, v := range inst.bindings {
if overload, ok := v.(*functions.Overload), ok {
overloads[overload.Operator] = overload
}
}
case *hierarchicalActivation:
overloads = Overloads(inst.parent)
override = Overloads(inst.child)
for k, v := range override {
overloads[k] = v
}
case *partActivation:
overloads = Overloads(inst.Activation)
}
return overloads
} in: https://github.com/google/cel-go/blob/master/cel/program.go#L301 overloads := Overloads(vars)
target := p.interpretable
if len(overloads) > 0 {
bindings := make([]*functions.Overload, len(overloads))
int i := 0
for _, binding := range overloads {
bindings[i] = binding
}
interpretable, err = interpreter.ReplaceBindings(intepretable, bindings, p.dispatcher)
if err != nil {
return nil, err
}
target = interpretable
}
v = target.Eval(vars) @mswest46 and @JimLarson I know this thread has gone cold, but I would be interested in understanding if a PR with the behaviour described above would be welcomed. If becomes really hard to implement a visitor for the Interpretable perhaps this option isn't really feasible. |
Currently, function definitions must be provided in the parsing step, so data relevant to their definition must be available at that step. This request is to support binding a function at activation time instead. As a use case example, I'd like to write a function
has_tag :: string -> bool
whose implementation checks membership in a listtags
that is only available at evaluation time and remains hidden from the expression writer.The text was updated successfully, but these errors were encountered: