/proc/self/cwd/eval/compiler/regex_precompilation_optimization.cc
Line | Count | Source |
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 | | // 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/regex_precompilation_optimization.h" |
16 | | |
17 | | #include <cstddef> |
18 | | #include <cstdint> |
19 | | #include <memory> |
20 | | #include <optional> |
21 | | #include <string> |
22 | | #include <utility> |
23 | | #include <vector> |
24 | | |
25 | | #include "absl/base/nullability.h" |
26 | | #include "absl/container/flat_hash_map.h" |
27 | | #include "absl/status/status.h" |
28 | | #include "absl/status/statusor.h" |
29 | | #include "absl/strings/string_view.h" |
30 | | #include "base/builtins.h" |
31 | | #include "common/ast.h" |
32 | | #include "common/expr.h" |
33 | | #include "common/native_type.h" |
34 | | #include "common/value.h" |
35 | | #include "eval/compiler/flat_expr_builder_extensions.h" |
36 | | #include "eval/eval/evaluator_core.h" |
37 | | #include "eval/eval/regex_match_step.h" |
38 | | #include "internal/re2_options.h" |
39 | | #include "internal/status_macros.h" |
40 | | #include "re2/re2.h" |
41 | | |
42 | | namespace google::api::expr::runtime { |
43 | | namespace { |
44 | | |
45 | | using ::cel::Ast; |
46 | | using ::cel::CallExpr; |
47 | | using ::cel::Expr; |
48 | | using ::cel::NativeTypeId; |
49 | | using ::cel::Reference; |
50 | | using ::cel::Value; |
51 | | |
52 | | using ReferenceMap = absl::flat_hash_map<int64_t, Reference>; |
53 | | |
54 | | bool IsFunctionOverload(const Expr& expr, absl::string_view function, |
55 | | absl::string_view overload, size_t arity, |
56 | 0 | const ReferenceMap& reference_map) { |
57 | 0 | if (!expr.has_call_expr()) { |
58 | 0 | return false; |
59 | 0 | } |
60 | 0 | const auto& call_expr = expr.call_expr(); |
61 | 0 | if (call_expr.function() != function) { |
62 | 0 | return false; |
63 | 0 | } |
64 | 0 | if (call_expr.args().size() + (call_expr.has_target() ? 1 : 0) != arity) { |
65 | 0 | return false; |
66 | 0 | } |
67 | | |
68 | | // If parse-only and opted in to the optimization, assume this is the intended |
69 | | // overload. This will still only change the evaluation plan if the second arg |
70 | | // is a constant string. |
71 | 0 | if (reference_map.empty()) { |
72 | 0 | return true; |
73 | 0 | } |
74 | | |
75 | 0 | auto reference = reference_map.find(expr.id()); |
76 | 0 | if (reference != reference_map.end() && |
77 | 0 | reference->second.overload_id().size() == 1 && |
78 | 0 | reference->second.overload_id().front() == overload) { |
79 | 0 | return true; |
80 | 0 | } |
81 | 0 | return false; |
82 | 0 | } |
83 | | |
84 | | // Abstraction for deduplicating regular expressions over the course of a single |
85 | | // create expression call. Should not be used during evaluation. Uses |
86 | | // std::shared_ptr and std::weak_ptr. |
87 | | class RegexProgramBuilder final { |
88 | | public: |
89 | | explicit RegexProgramBuilder(int max_program_size) |
90 | 0 | : max_program_size_(max_program_size) {} |
91 | | |
92 | | absl::StatusOr<std::shared_ptr<const RE2>> BuildRegexProgram( |
93 | 0 | std::string pattern) { |
94 | 0 | auto existing = programs_.find(pattern); |
95 | 0 | if (existing != programs_.end()) { |
96 | 0 | if (auto program = existing->second.lock(); program) { |
97 | 0 | return program; |
98 | 0 | } |
99 | 0 | programs_.erase(existing); |
100 | 0 | } |
101 | 0 | auto program = |
102 | 0 | std::make_shared<RE2>(pattern, cel::internal::MakeRE2Options()); |
103 | 0 | CEL_RETURN_IF_ERROR(cel::internal::CheckRE2(*program, max_program_size_)); |
104 | 0 | programs_.insert({std::move(pattern), program}); |
105 | 0 | return program; |
106 | 0 | } |
107 | | |
108 | | private: |
109 | | const int max_program_size_; |
110 | | absl::flat_hash_map<std::string, std::weak_ptr<const RE2>> programs_; |
111 | | }; |
112 | | |
113 | | class RegexPrecompilationOptimization : public ProgramOptimizer { |
114 | | public: |
115 | | explicit RegexPrecompilationOptimization(const ReferenceMap& reference_map, |
116 | | int regex_max_program_size) |
117 | 0 | : reference_map_(reference_map), |
118 | 0 | regex_program_builder_(regex_max_program_size) {} |
119 | | |
120 | 0 | absl::Status OnPreVisit(PlannerContext& context, const Expr& node) override { |
121 | 0 | return absl::OkStatus(); |
122 | 0 | } |
123 | | |
124 | 0 | absl::Status OnPostVisit(PlannerContext& context, const Expr& node) override { |
125 | | // Check that this is the correct matches overload instead of a user defined |
126 | | // overload. |
127 | 0 | if (!IsFunctionOverload(node, cel::builtin::kRegexMatch, "matches_string", |
128 | 0 | 2, reference_map_)) { |
129 | 0 | return absl::OkStatus(); |
130 | 0 | } |
131 | | |
132 | 0 | ProgramBuilder::Subexpression* subexpression = |
133 | 0 | context.program_builder().GetSubexpression(&node); |
134 | |
|
135 | 0 | const CallExpr& call_expr = node.call_expr(); |
136 | 0 | const Expr& pattern_expr = call_expr.args().back(); |
137 | | |
138 | | // Try to check if the regex is valid, whether or not we can actually update |
139 | | // the plan. |
140 | 0 | std::optional<std::string> pattern = |
141 | 0 | GetConstantString(context, subexpression, node, pattern_expr); |
142 | 0 | if (!pattern.has_value()) { |
143 | 0 | return absl::OkStatus(); |
144 | 0 | } |
145 | | |
146 | 0 | CEL_ASSIGN_OR_RETURN( |
147 | 0 | std::shared_ptr<const RE2> regex_program, |
148 | 0 | regex_program_builder_.BuildRegexProgram(std::move(pattern).value())); |
149 | |
|
150 | 0 | if (subexpression == nullptr || subexpression->IsFlattened()) { |
151 | | // Already modified, can't update further. |
152 | 0 | return absl::OkStatus(); |
153 | 0 | } |
154 | | |
155 | 0 | const Expr& subject_expr = |
156 | 0 | call_expr.has_target() ? call_expr.target() : call_expr.args().front(); |
157 | |
|
158 | 0 | return RewritePlan(context, subexpression, node, subject_expr, |
159 | 0 | std::move(regex_program)); |
160 | 0 | } |
161 | | |
162 | | private: |
163 | | std::optional<std::string> GetConstantString( |
164 | | PlannerContext& context, |
165 | | ProgramBuilder::Subexpression* absl_nullable subexpression, |
166 | 0 | const Expr& call_expr, const Expr& re_expr) const { |
167 | 0 | if (re_expr.has_const_expr() && re_expr.const_expr().has_string_value()) { |
168 | 0 | return re_expr.const_expr().string_value(); |
169 | 0 | } |
170 | | |
171 | 0 | if (subexpression == nullptr || subexpression->IsFlattened()) { |
172 | | // Already modified, can't recover the input pattern. |
173 | 0 | return std::nullopt; |
174 | 0 | } |
175 | 0 | std::optional<Value> constant; |
176 | | // otherwise stack-machine program. |
177 | 0 | ExecutionPathView re_plan = context.GetSubplan(re_expr); |
178 | 0 | if (re_plan.size() == 1) { |
179 | 0 | cel::Value val; |
180 | 0 | if (GetIfConstant(re_plan[0], val)) { |
181 | 0 | constant = std::move(val); |
182 | 0 | } |
183 | 0 | } |
184 | |
|
185 | 0 | if (constant.has_value() && constant->IsString()) { |
186 | 0 | return constant->GetString().ToString(); |
187 | 0 | } |
188 | | |
189 | 0 | return std::nullopt; |
190 | 0 | } |
191 | | |
192 | | absl::Status RewritePlan( |
193 | | PlannerContext& context, |
194 | | ProgramBuilder::Subexpression* absl_nonnull subexpression, |
195 | | const Expr& call, const Expr& subject, |
196 | 0 | std::shared_ptr<const RE2> regex_program) { |
197 | 0 | return RewriteStackMachinePlan(context, call, subject, |
198 | 0 | std::move(regex_program)); |
199 | 0 | } |
200 | | |
201 | | absl::Status RewriteStackMachinePlan( |
202 | | PlannerContext& context, const Expr& call, const Expr& subject, |
203 | 0 | std::shared_ptr<const RE2> regex_program) { |
204 | 0 | if (context.GetSubplan(subject).empty()) { |
205 | | // This subexpression was already optimized, nothing to do. |
206 | 0 | return absl::OkStatus(); |
207 | 0 | } |
208 | | |
209 | 0 | CEL_ASSIGN_OR_RETURN(ExecutionPath new_plan, |
210 | 0 | context.ExtractSubplan(subject)); |
211 | 0 | CEL_ASSIGN_OR_RETURN(auto step, |
212 | 0 | CreateRegexMatchStep(std::move(regex_program))); |
213 | 0 | new_plan.push_back( |
214 | 0 | ExpressionStep::MakeGenericStep(std::move(step), call.id())); |
215 | |
|
216 | 0 | return context.ReplaceSubplan(call, std::move(new_plan)); |
217 | 0 | } |
218 | | |
219 | | const ReferenceMap& reference_map_; |
220 | | RegexProgramBuilder regex_program_builder_; |
221 | | }; |
222 | | |
223 | | } // namespace |
224 | | |
225 | | ProgramOptimizerFactory CreateRegexPrecompilationExtension( |
226 | 0 | int regex_max_program_size) { |
227 | 0 | return [=](PlannerContext& context, const Ast& ast) { |
228 | 0 | return std::make_unique<RegexPrecompilationOptimization>( |
229 | 0 | ast.reference_map(), regex_max_program_size); |
230 | 0 | }; |
231 | 0 | } |
232 | | } // namespace google::api::expr::runtime |