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
4 changes: 2 additions & 2 deletions cel/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,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,
Expand Down Expand Up @@ -1017,7 +1017,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{}
Expand Down
9 changes: 6 additions & 3 deletions cel/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,14 +426,15 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) {
}
// 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
}
defer frame.Close()
mustClose = true
}
// Configure error recovery and details capture for evaluation.
defer func() {
Expand All @@ -446,6 +447,9 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) {
err = fmt.Errorf("internal error: %v", r)
}
}
if mustClose {
frame.Close()
}
}()

if p.observable != nil {
Expand Down Expand Up @@ -499,8 +503,7 @@ func (p *prog) newExecutionFrame(input any) (*interpreter.ExecutionFrame, error)
return nil, err
}
if p.defaultVars != nil {
// Update the frame's activation in place.
frame.Activation = interpreter.NewHierarchicalActivation(p.defaultVars, frame.Activation)
frame.SetDefaultVars(p.defaultVars)
}

return frame, nil
Expand Down
84 changes: 49 additions & 35 deletions ext/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -228,18 +231,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) {
Expand All @@ -253,16 +253,32 @@ type slotVal struct {
}

type dynamicSlotActivation struct {
cel.Activation
frame *interpreter.ExecutionFrame
slotExprs []interpreter.InterpretableV2
slotCount int
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`
Expand All @@ -283,11 +299,10 @@ func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) {
v.value = &val
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
Expand All @@ -311,40 +326,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))
return interpreter.EvalActivation(activation, b.Exec)
}

type constantSlotActivation struct {
cel.Activation
frame *interpreter.ExecutionFrame
slots traits.Lister
slotCount int
// ResolveName implements Activation interface method and proxies @index prefixed lookups into the slot
// activation associated with the block scope.
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 nil, false
}

// Unwrap returns the underlying activation.
func (sa *constantSlotActivation) Unwrap() cel.Activation {
return sa.Activation
// Parent implements the Activation interface.
func (b *constantBlock) Parent() cel.Activation {
return nil
}

// 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
}
return sa.Activation.ResolveName(name)
// 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) {
Expand Down
14 changes: 10 additions & 4 deletions interpreter/activation.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func EmptyActivation() Activation {
type emptyActivation struct{}

func (emptyActivation) ResolveName(string) (any, bool) { return nil, false }
func (emptyActivation) Parent() Activation { return nil }

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.
Expand Down Expand Up @@ -122,10 +123,15 @@ func (a *hierarchicalActivation) Parent() Activation {

// 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
if a.child != nil {
if object, found := a.child.ResolveName(name); found {
return object, found
}
}
return a.parent.ResolveName(name)
if a.parent != nil {
return a.parent.ResolveName(name)
}
return nil, false
}

// Unwrap returns the parent activation, stripping the local child scope.
Expand Down
2 changes: 1 addition & 1 deletion interpreter/async.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (fn *evalAsyncFunc) Args() []InterpretableV2 {

// Eval implements the Interpretable interface method.
func (fn *evalAsyncFunc) Eval(vars Activation) ref.Val {
return fn.Exec(AsFrame(vars))
return EvalActivation(vars, fn.Exec)
}

// Exec implements the InterpretableV2 interface method.
Expand Down
21 changes: 18 additions & 3 deletions interpreter/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,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)
}
}
Expand Down Expand Up @@ -1304,6 +1308,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() {
Expand All @@ -1312,14 +1322,17 @@ 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
isOpt = isOpt || qual.IsOptional()
if isOpt {
var present bool
qualObj, present, err = qual.QualifyIfPresent(vars, obj, false)
trackCostQualify(AsFrame(vars), qual.ID())
if hasFrame {
trackCostQualify(frame, qual.ID())
}
if err != nil {
return nil, false, err
}
Expand All @@ -1331,7 +1344,9 @@ func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, boo
}
} else {
qualObj, err = qual.Qualify(vars, obj)
trackCostQualify(AsFrame(vars), qual.ID())
if hasFrame {
trackCostQualify(frame, qual.ID())
}
if err != nil {
return nil, false, err
}
Expand Down
14 changes: 11 additions & 3 deletions interpreter/decorators.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,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)
}
Expand All @@ -265,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) {
Expand All @@ -279,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
Expand Down
Loading
Loading