Coverage Report

Created: 2026-05-27 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/proc/self/cwd/eval/compiler/constant_folding.cc
Line
Count
Source
1
// Copyright 2019 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
//     https://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
#include "eval/compiler/constant_folding.h"
16
17
#include <cstddef>
18
#include <memory>
19
#include <utility>
20
#include <vector>
21
22
#include "absl/base/attributes.h"
23
#include "absl/base/nullability.h"
24
#include "absl/status/status.h"
25
#include "absl/status/statusor.h"
26
#include "base/builtins.h"
27
#include "base/type_provider.h"
28
#include "common/ast.h"
29
#include "common/constant.h"
30
#include "common/expr.h"
31
#include "common/value.h"
32
#include "eval/compiler/flat_expr_builder_extensions.h"
33
#include "eval/compiler/resolver.h"
34
#include "eval/eval/const_value_step.h"
35
#include "eval/eval/evaluator_core.h"
36
#include "internal/status_macros.h"
37
#include "runtime/activation.h"
38
#include "runtime/internal/convert_constant.h"
39
#include "google/protobuf/arena.h"
40
#include "google/protobuf/descriptor.h"
41
#include "google/protobuf/message.h"
42
43
namespace cel::runtime_internal {
44
45
namespace {
46
47
using ::cel::Expr;
48
using ::cel::builtin::kAnd;
49
using ::cel::builtin::kOr;
50
using ::cel::builtin::kTernary;
51
using ::cel::runtime_internal::ConvertConstant;
52
using ::google::api::expr::runtime::CreateConstValueDirectStep;
53
using ::google::api::expr::runtime::CreateConstValueStep;
54
using ::google::api::expr::runtime::EvaluationListener;
55
using ::google::api::expr::runtime::ExecutionFrame;
56
using ::google::api::expr::runtime::ExecutionPath;
57
using ::google::api::expr::runtime::ExecutionPathView;
58
using ::google::api::expr::runtime::FlatExpressionEvaluatorState;
59
using ::google::api::expr::runtime::PlannerContext;
60
using ::google::api::expr::runtime::ProgramOptimizer;
61
using ::google::api::expr::runtime::ProgramOptimizerFactory;
62
using ::google::api::expr::runtime::Resolver;
63
64
enum class IsConst {
65
  kConditional,
66
  kNonConst,
67
};
68
69
class ConstantFoldingExtension : public ProgramOptimizer {
70
 public:
71
  ConstantFoldingExtension(
72
      const google::protobuf::DescriptorPool* absl_nonnull descriptor_pool,
73
      absl_nullable std::shared_ptr<google::protobuf::Arena> shared_arena,
74
      google::protobuf::Arena* absl_nonnull arena,
75
      absl_nullable std::shared_ptr<google::protobuf::MessageFactory>
76
          shared_message_factory,
77
      google::protobuf::MessageFactory* absl_nonnull message_factory,
78
      const TypeProvider& type_provider)
79
0
      : shared_arena_(std::move(shared_arena)),
80
0
        shared_message_factory_(std::move(shared_message_factory)),
81
0
        state_(kDefaultStackLimit, kComprehensionSlotCount, type_provider,
82
0
               descriptor_pool, message_factory, arena) {}
83
84
  absl::Status OnPreVisit(google::api::expr::runtime::PlannerContext& context,
85
                          const Expr& node) override;
86
  absl::Status OnPostVisit(google::api::expr::runtime::PlannerContext& context,
87
                           const Expr& node) override;
88
89
 private:
90
  // Most constant folding evaluations are simple
91
  // binary operators.
92
  static constexpr size_t kDefaultStackLimit = 4;
93
94
  // Comprehensions are not evaluated -- the current implementation can't detect
95
  // if the comprehension variables are only used in a const way.
96
  static constexpr size_t kComprehensionSlotCount = 0;
97
98
  absl_nullable std::shared_ptr<google::protobuf::Arena> shared_arena_;
99
  ABSL_ATTRIBUTE_UNUSED
100
  absl_nullable std::shared_ptr<google::protobuf::MessageFactory> shared_message_factory_;
101
  Activation empty_;
102
  FlatExpressionEvaluatorState state_;
103
104
  std::vector<IsConst> is_const_;
105
};
106
107
0
IsConst IsConstExpr(const Expr& expr, const Resolver& resolver) {
108
0
  switch (expr.kind_case()) {
109
0
    case ExprKindCase::kConstant:
110
0
      return IsConst::kConditional;
111
0
    case ExprKindCase::kIdentExpr:
112
0
      return IsConst::kNonConst;
113
0
    case ExprKindCase::kComprehensionExpr:
114
      // Not yet supported, need to identify whether range and
115
      // iter vars are compatible with const folding.
116
0
      return IsConst::kNonConst;
117
0
    case ExprKindCase::kStructExpr:
118
0
      return IsConst::kNonConst;
119
0
    case ExprKindCase::kMapExpr:
120
      // Empty maps are rare and not currently supported as they may eventually
121
      // have similar issues to empty list when used within comprehensions or
122
      // macros.
123
0
      if (expr.map_expr().entries().empty()) {
124
0
        return IsConst::kNonConst;
125
0
      }
126
0
      return IsConst::kConditional;
127
0
    case ExprKindCase::kListExpr:
128
0
      if (expr.list_expr().elements().empty()) {
129
        // Don't fold for empty list to allow comprehension
130
        // list append optimization.
131
0
        return IsConst::kNonConst;
132
0
      }
133
0
      return IsConst::kConditional;
134
0
    case ExprKindCase::kSelectExpr:
135
0
      return IsConst::kConditional;
136
0
    case ExprKindCase::kCallExpr: {
137
0
      const auto& call = expr.call_expr();
138
      // Short Circuiting operators not yet supported.
139
0
      if (call.function() == kAnd || call.function() == kOr ||
140
0
          call.function() == kTernary) {
141
0
        return IsConst::kNonConst;
142
0
      }
143
      // For now we skip constant folding for cel.@block. We do not yet setup
144
      // slots. When we enable constant folding for comprehensions (like
145
      // cel.bind), we can address cel.@block.
146
0
      if (call.function() == "cel.@block") {
147
0
        return IsConst::kNonConst;
148
0
      }
149
150
0
      int arg_len = call.args().size() + (call.has_target() ? 1 : 0);
151
      // Check for any lazy overloads (activation dependant)
152
0
      if (!resolver
153
0
               .FindLazyOverloads(call.function(), call.has_target(), arg_len)
154
0
               .empty()) {
155
0
        return IsConst::kNonConst;
156
0
      }
157
158
0
      auto overloads =
159
0
          resolver.FindOverloads(call.function(), call.has_target(), arg_len);
160
      // Check for any contextual overloads. If there are any, we cowardly
161
      // avoid constant folding instead of trying to check if one of the
162
      // overloads would be safe to use.
163
0
      for (const auto& overload : overloads) {
164
0
        if (overload.descriptor.is_contextual()) {
165
0
          return IsConst::kNonConst;
166
0
        }
167
0
      }
168
169
0
      return IsConst::kConditional;
170
0
    }
171
0
    case ExprKindCase::kUnspecifiedExpr:
172
0
    default:
173
0
      return IsConst::kNonConst;
174
0
  }
175
0
}
176
177
absl::Status ConstantFoldingExtension::OnPreVisit(PlannerContext& context,
178
0
                                                  const Expr& node) {
179
0
  IsConst is_const = IsConstExpr(node, context.resolver());
180
0
  is_const_.push_back(is_const);
181
182
0
  return absl::OkStatus();
183
0
}
184
185
absl::Status ConstantFoldingExtension::OnPostVisit(PlannerContext& context,
186
0
                                                   const Expr& node) {
187
0
  if (is_const_.empty()) {
188
0
    return absl::InternalError("ConstantFoldingExtension called out of order.");
189
0
  }
190
191
0
  IsConst is_const = is_const_.back();
192
0
  is_const_.pop_back();
193
194
0
  if (is_const == IsConst::kNonConst) {
195
    // update parent
196
0
    if (!is_const_.empty()) {
197
0
      is_const_.back() = IsConst::kNonConst;
198
0
    }
199
0
    return absl::OkStatus();
200
0
  }
201
0
  ExecutionPathView subplan = context.GetSubplan(node);
202
0
  if (subplan.empty()) {
203
    // This subexpression is already optimized out or suppressed.
204
0
    return absl::OkStatus();
205
0
  }
206
  // copy string to managed handle if backed by the original program.
207
0
  Value value;
208
0
  if (node.has_const_expr()) {
209
0
    CEL_ASSIGN_OR_RETURN(value,
210
0
                         ConvertConstant(node.const_expr(), state_.arena()));
211
0
  } else {
212
0
    ExecutionFrame frame(subplan, empty_, context.options(), state_);
213
0
    state_.Reset();
214
    // Update stack size to accommodate sub expression.
215
    // This only results in a vector resize if the new maxsize is greater than
216
    // the current capacity.
217
0
    state_.value_stack().SetMaxSize(subplan.size());
218
219
0
    auto result = frame.Evaluate();
220
    // If this would be a runtime error, then don't adjust the program plan, but
221
    // rather allow the error to occur at runtime to preserve the evaluation
222
    // contract with non-constant folding use cases.
223
0
    if (!result.ok()) {
224
0
      return absl::OkStatus();
225
0
    }
226
0
    value = *result;
227
0
    if (value->Is<UnknownValue>()) {
228
0
      return absl::OkStatus();
229
0
    }
230
0
  }
231
232
  // If recursive planning enabled (recursion limit unbounded or at least 1),
233
  // use a recursive (direct) step for the folded constant.
234
  //
235
  // Constant folding is applied leaf to root based on the program plan so far,
236
  // so the planner will have an opportunity to validate that the recursion
237
  // limit is being followed when visiting parent nodes in the AST.
238
0
  if (context.options().max_recursion_depth != 0) {
239
0
    return context.ReplaceSubplan(
240
0
        node, CreateConstValueDirectStep(std::move(value), node.id()), 1);
241
0
  }
242
243
  // Otherwise make a stack machine plan.
244
0
  ExecutionPath new_plan;
245
0
  CEL_ASSIGN_OR_RETURN(
246
0
      new_plan.emplace_back(),
247
0
      CreateConstValueStep(std::move(value), node.id(), false));
248
249
0
  return context.ReplaceSubplan(node, std::move(new_plan));
250
0
}
251
252
}  // namespace
253
254
ProgramOptimizerFactory CreateConstantFoldingOptimizer(
255
    absl_nullable std::shared_ptr<google::protobuf::Arena> arena,
256
0
    absl_nullable std::shared_ptr<google::protobuf::MessageFactory> message_factory) {
257
0
  return
258
0
      [shared_arena = std::move(arena),
259
0
       shared_message_factory = std::move(message_factory)](
260
0
          PlannerContext& context,
261
0
          const Ast&) -> absl::StatusOr<std::unique_ptr<ProgramOptimizer>> {
262
        // If one was explicitly provided during planning or none was explicitly
263
        // provided during configuration, request one from the planning context.
264
        // Otherwise use the one provided during configuration.
265
0
        google::protobuf::Arena* absl_nonnull arena =
266
0
            context.HasExplicitArena() || shared_arena == nullptr
267
0
                ? context.MutableArena()
268
0
                : shared_arena.get();
269
0
        google::protobuf::MessageFactory* absl_nonnull message_factory =
270
0
            context.HasExplicitMessageFactory() ||
271
0
                    shared_message_factory == nullptr
272
0
                ? context.MutableMessageFactory()
273
0
                : shared_message_factory.get();
274
0
        return std::make_unique<ConstantFoldingExtension>(
275
0
            context.descriptor_pool(), shared_arena, arena,
276
0
            shared_message_factory, message_factory, context.type_reflector());
277
0
      };
278
0
}
279
280
}  // namespace cel::runtime_internal