Skip to content

Commit 5db3640

Browse files
Inlining optimizer (#827)
Inliner with support for identifiers and simple select expressions Note: optional field selections are not supported for inlining matches at this time.
1 parent 8943046 commit 5db3640

File tree

3 files changed

+870
-0
lines changed

3 files changed

+870
-0
lines changed

cel/inlining.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cel
16+
17+
import (
18+
"github.com/google/cel-go/common/ast"
19+
"github.com/google/cel-go/common/containers"
20+
"github.com/google/cel-go/common/operators"
21+
"github.com/google/cel-go/common/overloads"
22+
"github.com/google/cel-go/common/types"
23+
"github.com/google/cel-go/common/types/traits"
24+
)
25+
26+
// InlineVariable holds a variable name to be matched and an AST representing
27+
// the expression graph which should be used to replace it.
28+
type InlineVariable struct {
29+
name string
30+
alias string
31+
def *ast.AST
32+
}
33+
34+
// Name returns the qualified variable or field selection to replace.
35+
func (v *InlineVariable) Name() string {
36+
return v.name
37+
}
38+
39+
// Alias returns the alias to use when performing cel.bind() calls during inlining.
40+
func (v *InlineVariable) Alias() string {
41+
return v.alias
42+
}
43+
44+
// Expr returns the inlined expression value.
45+
func (v *InlineVariable) Expr() ast.Expr {
46+
return v.def.Expr()
47+
}
48+
49+
// Type indicates the inlined expression type.
50+
func (v *InlineVariable) Type() *Type {
51+
return v.def.GetType(v.def.Expr().ID())
52+
}
53+
54+
// NewInlineVariable declares a variable name to be replaced by a checked expression.
55+
func NewInlineVariable(name string, definition *Ast) *InlineVariable {
56+
return NewInlineVariableWithAlias(name, name, definition)
57+
}
58+
59+
// NewInlineVariableWithAlias declares a variable name to be replaced by a checked expression.
60+
// If the variable occurs more than once, the provided alias will be used to replace the expressions
61+
// where the variable name occurs.
62+
func NewInlineVariableWithAlias(name, alias string, definition *Ast) *InlineVariable {
63+
return &InlineVariable{name: name, alias: alias, def: definition.impl}
64+
}
65+
66+
// NewInliningOptimizer creates and optimizer which replaces variables with expression definitions.
67+
//
68+
// If a variable occurs one time, the variable is replaced by the inline definition. If the
69+
// variable occurs more than once, the variable occurences are replaced by a cel.bind() call.
70+
func NewInliningOptimizer(inlineVars ...*InlineVariable) ASTOptimizer {
71+
return &inliningOptimizer{variables: inlineVars}
72+
}
73+
74+
type inliningOptimizer struct {
75+
variables []*InlineVariable
76+
}
77+
78+
func (opt *inliningOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) *ast.AST {
79+
root := ast.NavigateAST(a)
80+
for _, inlineVar := range opt.variables {
81+
matches := ast.MatchDescendants(root, opt.matchVariable(inlineVar.Name()))
82+
// Skip cases where the variable isn't in the expression graph
83+
if len(matches) == 0 {
84+
continue
85+
}
86+
87+
// For a single match, do a direct replacement of the expression sub-graph.
88+
if len(matches) == 1 {
89+
opt.inlineExpr(ctx, matches[0], ctx.CopyExpr(inlineVar.Expr()), inlineVar.Type())
90+
continue
91+
}
92+
93+
if !isBindable(matches, inlineVar.Expr(), inlineVar.Type()) {
94+
for _, match := range matches {
95+
opt.inlineExpr(ctx, match, ctx.CopyExpr(inlineVar.Expr()), inlineVar.Type())
96+
}
97+
continue
98+
}
99+
// For multiple matches, find the least common ancestor (lca) and insert the
100+
// variable as a cel.bind() macro.
101+
var lca ast.NavigableExpr = nil
102+
ancestors := map[int64]bool{}
103+
for _, match := range matches {
104+
// Update the identifier matches with the provided alias.
105+
aliasExpr := ctx.NewIdent(inlineVar.Alias())
106+
opt.inlineExpr(ctx, match, aliasExpr, inlineVar.Type())
107+
parent, found := match, true
108+
for found {
109+
_, hasAncestor := ancestors[parent.ID()]
110+
if hasAncestor && (lca == nil || lca.Depth() < parent.Depth()) {
111+
lca = parent
112+
}
113+
ancestors[parent.ID()] = true
114+
parent, found = parent.Parent()
115+
}
116+
}
117+
118+
// Update the least common ancestor by inserting a cel.bind() call to the alias.
119+
inlined := ctx.NewBindMacro(lca.ID(), inlineVar.Alias(), inlineVar.Expr(), lca)
120+
opt.inlineExpr(ctx, lca, inlined, inlineVar.Type())
121+
}
122+
return a
123+
}
124+
125+
// inlineExpr replaces the current expression with the inlined one, unless the location of the inlining
126+
// happens within a presence test, e.g. has(a.b.c) -> inline alpha for a.b.c in which case an attempt is
127+
// made to determine whether the inlined value can be presence or existence tested.
128+
func (opt *inliningOptimizer) inlineExpr(ctx *OptimizerContext, prev, inlined ast.Expr, inlinedType *Type) {
129+
switch prev.Kind() {
130+
case ast.SelectKind:
131+
sel := prev.AsSelect()
132+
if !sel.IsTestOnly() {
133+
prev.SetKindCase(inlined)
134+
return
135+
}
136+
opt.rewritePresenceExpr(ctx, prev, inlined, inlinedType)
137+
default:
138+
prev.SetKindCase(inlined)
139+
}
140+
}
141+
142+
// rewritePresenceExpr converts the inlined expression, when it occurs within a has() macro, to type-safe
143+
// expression appropriate for the inlined type, if possible.
144+
//
145+
// If the rewrite is not possible an error is reported at the inline expression site.
146+
func (opt *inliningOptimizer) rewritePresenceExpr(ctx *OptimizerContext, prev, inlined ast.Expr, inlinedType *Type) {
147+
// If the input inlined expression is not a select expression it won't work with the has()
148+
// macro. Attempt to rewrite the presence test in terms of the typed input, otherwise error.
149+
ctx.sourceInfo.ClearMacroCall(prev.ID())
150+
if inlined.Kind() == ast.SelectKind {
151+
inlinedSel := inlined.AsSelect()
152+
prev.SetKindCase(
153+
ctx.NewPresenceTest(prev.ID(), inlinedSel.Operand(), inlinedSel.FieldName()))
154+
return
155+
}
156+
if inlinedType.IsAssignableType(NullType) {
157+
prev.SetKindCase(
158+
ctx.NewCall(operators.NotEquals,
159+
inlined,
160+
ctx.NewLiteral(types.NullValue),
161+
))
162+
return
163+
}
164+
if inlinedType.HasTrait(traits.SizerType) {
165+
prev.SetKindCase(
166+
ctx.NewCall(operators.NotEquals,
167+
ctx.NewMemberCall(overloads.Size, inlined),
168+
ctx.NewLiteral(types.IntZero),
169+
))
170+
return
171+
}
172+
ctx.ReportErrorAtID(prev.ID(), "unable to inline expression type %v into presence test", inlinedType)
173+
}
174+
175+
// isBindable indicates whether the inlined type can be used within a cel.bind() if the expression
176+
// being replaced occurs within a presence test. Value types with a size() method or field selection
177+
// support can be bound.
178+
//
179+
// In future iterations, support may also be added for indexer types which can be rewritten as an `in`
180+
// expression; however, this would imply a rewrite of the inlined expression that may not be necessary
181+
// in most cases.
182+
func isBindable(matches []ast.NavigableExpr, inlined ast.Expr, inlinedType *Type) bool {
183+
if inlinedType.IsAssignableType(NullType) ||
184+
inlinedType.HasTrait(traits.SizerType) ||
185+
inlinedType.HasTrait(traits.FieldTesterType) {
186+
return true
187+
}
188+
for _, m := range matches {
189+
if m.Kind() != ast.SelectKind {
190+
continue
191+
}
192+
sel := m.AsSelect()
193+
if sel.IsTestOnly() {
194+
return false
195+
}
196+
}
197+
return true
198+
}
199+
200+
// matchVariable matches simple identifiers, select expressions, and presence test expressions
201+
// which match the (potentially) qualified variable name provided as input.
202+
//
203+
// Note, this function does not support inlining against select expressions which includes optional
204+
// field selection. This may be a future refinement.
205+
func (opt *inliningOptimizer) matchVariable(varName string) ast.ExprMatcher {
206+
return func(e ast.NavigableExpr) bool {
207+
if e.Kind() == ast.IdentKind && e.AsIdent() == varName {
208+
return true
209+
}
210+
if e.Kind() == ast.SelectKind {
211+
sel := e.AsSelect()
212+
// While the `ToQualifiedName` call could take the select directly, this
213+
// would skip presence tests from possible matches, which we would like
214+
// to include.
215+
qualName, found := containers.ToQualifiedName(sel.Operand())
216+
return found && qualName+"."+sel.FieldName() == varName
217+
}
218+
return false
219+
}
220+
}

0 commit comments

Comments
 (0)