From 6eaa65523ab2d802ff270f24f78bae0b89324b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20=C5=A0till?= Date: Thu, 10 Oct 2024 13:42:33 +0200 Subject: [PATCH] Add utilities for running modifications inside nested IR nodes (#4940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add ir-traversal Signed-off-by: Vladimír Štill * Docs for ir-traversal Signed-off-by: Vladimír Štill * Further docs tweaks Signed-off-by: Vladimír Štill * Code tweaks Signed-off-by: Vladimír Štill * Fix formatting Signed-off-by: Vladimír Štill * Add tests, few fixes in impl Signed-off-by: Vladimír Štill * Fix a typo & make lint happy Signed-off-by: Vladimír Štill * Format Signed-off-by: Vladimir Still * Put the namespace Detail into a separate file Signed-off-by: Vladimír Štill * Exemplify IR::Traversal on few places in testgen Signed-off-by: Vladimír Štill * Try using quote includes Signed-off-by: Vladimír Štill --------- Signed-off-by: Vladimír Štill Signed-off-by: Vladimir Still --- .../core/small_step/abstract_stepper.cpp | 9 +- .../testgen/core/small_step/expr_stepper.cpp | 28 ++-- .../core/small_step/extern_stepper.cpp | 25 ++-- ir/CMakeLists.txt | 2 + ir/ir-traversal-internal.h | 108 ++++++++++++++ ir/ir-traversal.h | 135 ++++++++++++++++++ test/CMakeLists.txt | 1 + test/gtest/ir-traversal-test.cpp | 95 ++++++++++++ 8 files changed, 373 insertions(+), 30 deletions(-) create mode 100644 ir/ir-traversal-internal.h create mode 100644 ir/ir-traversal.h create mode 100644 test/gtest/ir-traversal-test.cpp diff --git a/backends/p4tools/modules/testgen/core/small_step/abstract_stepper.cpp b/backends/p4tools/modules/testgen/core/small_step/abstract_stepper.cpp index c50784ffc7..7121564f77 100644 --- a/backends/p4tools/modules/testgen/core/small_step/abstract_stepper.cpp +++ b/backends/p4tools/modules/testgen/core/small_step/abstract_stepper.cpp @@ -14,6 +14,7 @@ #include "ir/dump.h" #include "ir/id.h" #include "ir/indexed_vector.h" +#include "ir/ir-traversal.h" #include "ir/irutils.h" #include "lib/cstring.h" #include "lib/exceptions.h" @@ -171,10 +172,10 @@ bool AbstractStepper::stepToStructSubexpr( return stepToSubexpr( nonValueComponent, result, state, [nonValueComponentIdx, rebuildCmd, subexpr](const Continuation::Parameter *v) { - auto *result = subexpr->clone(); - auto *namedClone = result->components[nonValueComponentIdx]->clone(); - namedClone->expression = v->param; - result->components[nonValueComponentIdx] = namedClone; + auto *result = IR::Traversal::apply(subexpr, &IR::StructExpression::components, + IR::Traversal::Index(nonValueComponentIdx), + &IR::NamedExpression::expression, + IR::Traversal::Assign(v->param)); return rebuildCmd(result); }); } diff --git a/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp b/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp index e89562389d..f10766105b 100644 --- a/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp +++ b/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp @@ -12,6 +12,7 @@ #include "backends/p4tools/common/lib/trace_event_types.h" #include "backends/p4tools/common/lib/variables.h" #include "ir/declaration.h" +#include "ir/ir-traversal.h" #include "ir/irutils.h" #include "ir/node.h" #include "ir/solver.h" @@ -137,23 +138,22 @@ bool ExprStepper::resolveMethodCallArguments(const IR::MethodCallExpression *cal // the argument is not yet symbolic, try to resolve it. return stepToSubexpr( argExpr, result, state, [call, idx, param](const Continuation::Parameter *v) { - // TODO: It seems expensive to copy the function every time we resolve an argument. + // TODO: It seems expensive to copy the function call every time we resolve an + // argument. // We should do this all at once. But how? // This is the same problem as in stepToListSubexpr // Thankfully, most method calls have less than 10 arguments. - auto *clonedCall = call->clone(); - auto *arguments = clonedCall->arguments->clone(); - auto *arg = arguments->at(idx)->clone(); - const IR::Expression *computedExpr = v->param; - // A parameter with direction InOut might be read and also written to. - // We capture this ambiguity with an InOutReference. - if (param->direction == IR::Direction::InOut) { - auto stateVar = ToolsVariables::convertReference(arg->expression); - computedExpr = new IR::InOutReference(stateVar, computedExpr); - } - arg->expression = computedExpr; - (*arguments)[idx] = arg; - clonedCall->arguments = arguments; + auto *clonedCall = IR::Traversal::apply( + call, &IR::MethodCallExpression::arguments, IR::Traversal::Index(idx), + &IR::Argument::expression, [&](IR::Expression *expr) -> const IR::Expression * { + // A parameter with direction InOut might be read and also written to. + // We capture this ambiguity with an InOutReference. + if (param->direction == IR::Direction::InOut) { + auto stateVar = ToolsVariables::convertReference(expr); + return new IR::InOutReference(stateVar, v->param); + } + return v->param; + }); return Continuation::Return(clonedCall); }); } diff --git a/backends/p4tools/modules/testgen/core/small_step/extern_stepper.cpp b/backends/p4tools/modules/testgen/core/small_step/extern_stepper.cpp index 2b8b9ae408..8cb10a1ff1 100644 --- a/backends/p4tools/modules/testgen/core/small_step/extern_stepper.cpp +++ b/backends/p4tools/modules/testgen/core/small_step/extern_stepper.cpp @@ -14,6 +14,7 @@ #include "backends/p4tools/common/lib/trace_event_types.h" #include "backends/p4tools/common/lib/variables.h" #include "ir/id.h" +#include "ir/ir-traversal.h" #include "ir/ir.h" #include "ir/irutils.h" #include "ir/vector.h" @@ -32,6 +33,14 @@ namespace P4::P4Tools::P4Testgen { +/// Replace argument of given index in a method call with given value. +const IR::MethodCallExpression *replaceCallArg(const IR::MethodCallExpression *call, size_t argIdx, + const IR::Expression *replacement) { + return IR::Traversal::apply(call, &IR::MethodCallExpression::arguments, + IR::Traversal::Index(argIdx), &IR::Argument::expression, + IR::Traversal::Assign(replacement)); +} + std::vector> ExprStepper::setFields( ExecutionState &nextState, const std::vector &flatFields, int varBitFieldSize) { @@ -392,12 +401,8 @@ const ExprStepper::ExternMethodImpls ExprStepper::CORE_EXTERN_METHO if (!SymbolicEnv::isSymbolicValue(advanceExpr)) { stepToSubexpr(advanceExpr, stepper.result, stepper.state, [&externInfo](const Continuation::Parameter *v) { - auto *clonedCall = externInfo.originalCall.clone(); - auto *arguments = clonedCall->arguments->clone(); - auto *arg = arguments->at(0)->clone(); - arg->expression = v->param; - (*arguments)[0] = arg; - clonedCall->arguments = arguments; + const auto *clonedCall = + replaceCallArg(&externInfo.originalCall, 0, v->param); return Continuation::Return(clonedCall); }); return; @@ -541,12 +546,8 @@ const ExprStepper::ExternMethodImpls ExprStepper::CORE_EXTERN_METHO if (!SymbolicEnv::isSymbolicValue(varbitExtractExpr)) { stepToSubexpr(varbitExtractExpr, stepper.result, stepper.state, [&externInfo](const Continuation::Parameter *v) { - auto *clonedCall = externInfo.originalCall.clone(); - auto *arguments = clonedCall->arguments->clone(); - auto *arg = arguments->at(1)->clone(); - arg->expression = v->param; - (*arguments)[1] = arg; - clonedCall->arguments = arguments; + const auto *clonedCall = + replaceCallArg(&externInfo.originalCall, 1, v->param); return Continuation::Return(clonedCall); }); return; diff --git a/ir/CMakeLists.txt b/ir/CMakeLists.txt index 9ae8a25324..bcd16044c5 100644 --- a/ir/CMakeLists.txt +++ b/ir/CMakeLists.txt @@ -43,6 +43,8 @@ set (IR_HDRS ir-inline.h ir-tree-macros.h ir.h + ir-traversal.h + ir-traversal-internal.h irutils.h json_generator.h json_loader.h diff --git a/ir/ir-traversal-internal.h b/ir/ir-traversal-internal.h new file mode 100644 index 0000000000..212a61beeb --- /dev/null +++ b/ir/ir-traversal-internal.h @@ -0,0 +1,108 @@ +/* +Copyright 2024-present Intel Corporation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/** + * \file + * \brief IR traversing utilities, internal implementation details, not to be used directly. + * \date 2024 + * \author Vladimír Štill + */ + +#ifndef IR_IR_TRAVERSAL_INTERNAL_H_ +#define IR_IR_TRAVERSAL_INTERNAL_H_ + +#include "lib/exceptions.h" +#include "lib/rtti_utils.h" + +#ifndef IR_IR_TRAVERSAL_INTERNAL_ENABLE +#error "This file must not be used directly" +#endif + +namespace P4::IR::Traversal::Detail { + +/// @brief Internal, don't use directly. +/// The class exists so that the functions can be mutually recursive. +struct Traverse { + template + static const Obj *modify(const Obj *, Assign asgn) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + return asgn.value; + } + + template + static const Obj *modify(Obj *, Assign asgn) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + return asgn.value; + } + + template + static Obj *modify(Obj *obj, Assign &&asgn) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + *obj = std::move(asgn.value); + return obj; + } + + template + static Obj *modify(Obj *obj, const Assign &asgn) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + *obj = asgn.value; + return obj; + } + + template + static decltype(auto) modify(Obj *obj, Fn fn) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + return fn(obj); + } + + template + static Obj *modify(Obj *obj, Index idx, Selectors &&...selectors) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + BUG_CHECK(obj->size() > idx.value, "Index %1% out of bounds of %2%", idx.value, obj); + modifyRef((*obj)[idx.value], std::forward(selectors)...); + return obj; + } + + template + static To *modify(Obj *obj, RTTI::Detail::ToType cast, Selectors &&...selectors) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + auto *casted = cast(obj); + BUG_CHECK(casted, "Cast of %1% failed", obj); + return modify(casted, std::forward(selectors)...); + } + + template + static Obj *modify(Obj *obj, Sub T::*member, Selectors &&...selectors) { + static_assert(!std::is_const_v, "Cannot modify constant object"); + modifyRef(obj->*member, std::forward(selectors)...); + return obj; + } + + template + static void modifyRef(T &ref, Selectors &&...selectors) { + if constexpr (std::is_pointer_v) { + ref = modify(ref->clone(), std::forward(selectors)...); + } else { + auto *res = modify(&ref, std::forward(selectors)...); + if (&ref != res) { + ref = *res; + } + } + } +}; + +} // namespace P4::IR::Traversal::Detail + +#endif // IR_IR_TRAVERSAL_INTERNAL_H_ diff --git a/ir/ir-traversal.h b/ir/ir-traversal.h new file mode 100644 index 0000000000..791d298e10 --- /dev/null +++ b/ir/ir-traversal.h @@ -0,0 +1,135 @@ +/* +Copyright 2024-present Intel Corporation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/** + * \file + * \brief IR traversing utilities. This is mainly useful for modifying a sub-component of an IR + * node. + * \date 2024 + * \author Vladimír Štill + */ + +#ifndef IR_IR_TRAVERSAL_H_ +#define IR_IR_TRAVERSAL_H_ +#define IR_IR_TRAVERSAL_INTERNAL_ENABLE + +#include + +namespace P4::IR::Traversal { + +/// @brief A selector used at the end of selector chain to assign to the current sub-object. +/// e.g. `modify(obj, &IR::AssignmentStatement::left, Assign(var))` will set the LHS of assignment. +/// @tparam T the parameter is usually derived by the C++ compiler. +template +struct Assign { + explicit Assign(const T &value) : value(value) {} + T value; +}; + +/// @brief Select an index of a integer-indexed sub-object. %This is useful e.g. to select first +/// parameter of a method call and in similar cases involving @ref IR::Vector or @ref +/// IR::IndexedVector. +/// +/// @code +/// modify(mce, &IR::MethodCallExpression::arguments, Index(0), &IR::Argument::expression, +/// Assign(val)) +/// @endcode +/// This snippet assigns `val` into the first argument of a method call. +struct Index { + explicit Index(size_t value) : value(value) {} + size_t value; +}; + +} // namespace P4::IR::Traversal + +// Put the internals out of the main file to increase its readability. +#include "ir-traversal-internal.h" + +namespace P4::IR::Traversal { + +/// @brief Given an object @p obj and a series of selector (ending with a modifier), modify the @p +/// obj's sub object at the selected path. %This is useful for deeper modification of objects that +/// is not based on object types (in that case please use visitors) but on object structure (e.g. +/// member paths of C++ objects). +/// @param obj An object to modify. +/// @param ...selectors A series of selectors ending either with Assign, or with a modifier +/// function (typically a lambda). +/// @return Modified object. This will usually be the same object as @p obj (unless the only +/// selector is Assign with a pointer). +/// +/// @section sec_selectors Path Selectors +/// +/// Selection of the sub-objects is perfomed mainly using member pointers, but other selectors are +/// also possible. +/// +/// - Object member pointer (e.g. `&IR::Expression::type`, `&IR::AssignmentStatement::left`) -- +/// these are used to select members of object. Note that since the IR sometimes uses inline +/// members and sometimes uses pointer members these are transparently handled the same -- this +/// behaves as-if at any point the current object was a pointer to value that is to be processed +/// by the following selectors. +/// - @ref `IR::Traversal::Index` -- used for indexing e.g. @ref `IR::Vector` or @ref +/// `IR::IndexedVector`. +/// - @ref `RTTI::to` -- used to cast to a more specific type. This is necessary to access +/// members of the more specific type (e.g. if you know that a RHS of `IR::AssignmentStatement` is +/// `IR::Lss`, you can use `RTTI::to` and then access members of `IR::Lss`. Note that the +/// cast is not applied to anything -- we are passing a cast object. +/// +/// @section sec_modifiers Modifiers +/// +/// Modifier is a last component of the chain, it modifies the selected object. +/// - @ref `Traversal::Assign` -- a simple modifier that assigns/replaces the currently selected +/// value. If the argument is a pointer, the value is replaced, if the argument is non-pointer, +/// the selected value is assigned. +/// - freeform modifier object -- usually a lambda. This object will receive pointer to non-const +/// value of the currently selected object and can modify it in any way (e.g. increment value). +/// +/// For example, given an extern call, you can modify the type of the first argument by adding a +/// cast to it like this: +/// @code +/// modify(call, &IR::MethodCallStatement::methodCall, &IR::MethodCallExpression::arguments, +/// Index(0), &IR::Argument::expression, [&](const IR::Expression *expr) +/// { +/// return new IR::Cast(expr->srcInfo, idxType, expr); +/// }); +/// @endcode +/// +/// Similarly, you can set the type of the first argument in the type of the extern like this: +/// @code +/// modify(call, &IR::MethodCallStatement::methodCall, &IR::MethodCallExpression::method, +/// &IR::Expression::type, RTTI::to, &IR::Type_Method::parameters, +/// &IR::ParameterList::parameters, Index(0), &IR::Parameter::type, Assign(idxType)); +/// @endcode +/// +/// Any time a cast fails or an index is out of range the modification triggers a `BUG`. +/// +/// Please note that any modification is applied only to the selected path, that is, if the same +/// node occurs multiple times in the IR DAG, and one of them is "focused" by the selector and +/// modified, the other instance is unchanged (because the "focused" instance is cloned). +template +Obj *modify(Obj *obj, Selectors &&...selectors) { + return Detail::Traverse::modify(obj, std::forward(selectors)...); +} + +/// @brief Similar to modify, but accepts constant argument which is cloned. Therefore, the result +/// is a different object then @p obj. @sa `Traversal::modify`. +template +Obj *apply(const Obj *obj, Selectors &&...selectors) { + return Detail::Traverse::modify(obj->clone(), std::forward(selectors)...); +} + +} // namespace P4::IR::Traversal + +#undef IR_IR_TRAVERSAL_INTERNAL_ENABLE +#endif // IR_IR_TRAVERSAL_H_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 83a414ab12..b0b4ebc6ba 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -38,6 +38,7 @@ set (GTEST_UNITTEST_SOURCES gtest/hvec_map.cpp gtest/hvec_set.cpp gtest/indexed_vector.cpp + gtest/ir-traversal-test.cpp gtest/json_test.cpp gtest/map.cpp gtest/midend_def_use.cpp diff --git a/test/gtest/ir-traversal-test.cpp b/test/gtest/ir-traversal-test.cpp new file mode 100644 index 0000000000..a7d6f07c52 --- /dev/null +++ b/test/gtest/ir-traversal-test.cpp @@ -0,0 +1,95 @@ +/* +Copyright 2024-present Intel Corporation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "ir/ir-traversal.h" + +#include + +#include "ir/ir.h" + +namespace P4::Test { + +struct TraversalTest : public ::testing::Test {}; + +TEST_F(TraversalTest, SimpleIRApply) { + const auto *v = new IR::Constant(42); + const auto *m = IR::Traversal::apply(v, &IR::Constant::value, IR::Traversal::Assign(4)); + EXPECT_NE(v, m); + EXPECT_EQ(v->value, 42); + EXPECT_EQ(m->value, 4); + + const auto *t = IR::Type_Bits::get(4); + const auto *m2 = IR::Traversal::apply(v, &IR::Constant::type, IR::Traversal::Assign(t)); + EXPECT_NE(v, m2); + EXPECT_EQ(v->value, 42); + EXPECT_EQ(m2->value, 42); + EXPECT_EQ(m2->type, t); +} + +TEST_F(TraversalTest, SimpleIRModify) { + auto *v = new IR::Constant(42); + auto *m = IR::Traversal::modify(v, &IR::Constant::value, IR::Traversal::Assign(4)); + EXPECT_EQ(v, m); + EXPECT_EQ(v->value, 4); + + const auto *t = IR::Type_Bits::get(4); + const auto *m2 = IR::Traversal::modify(v, &IR::Constant::type, IR::Traversal::Assign(t)); + EXPECT_EQ(v, m2); + EXPECT_EQ(v->value, 4); + EXPECT_EQ(v->type, t); +} + +TEST_F(TraversalTest, ComplexIRModify) { + const auto *path = new IR::Path("foo"); + const auto *pe = new IR::PathExpression(path); + const auto *add = new IR::Add(pe, new IR::Constant(42)); + auto *asgn = new IR::AssignmentStatement(pe, add); + IR::StatOrDecl *stmt = asgn; + + modify(stmt, RTTI::to, &IR::AssignmentStatement::right, + RTTI::to, &IR::Operation_Binary::left, + RTTI::to, &IR::PathExpression::path, &IR::Path::name, &IR::ID::name, + IR::Traversal::Assign(P4::cstring("bar"))); + // original not modified + EXPECT_EQ(path->name.name, "foo"); + EXPECT_EQ(asgn->left->checkedTo()->path->name.name, "foo"); + EXPECT_EQ( + asgn->right->checkedTo()->left->checkedTo()->path->name.name, + "bar"); +} + +struct S0 { + int v0 = 0; + int v1 = 1; +}; + +struct S1 { + int v0 = 2; + S0 s0; +}; + +TEST_F(TraversalTest, SimpleNonIRModify) { + S1 s1; + modify(&s1, &S1::s0, &S0::v1, IR::Traversal::Assign(4)); + EXPECT_EQ(s1.s0.v1, 4); + IR::Traversal::modify(&s1, &S1::s0, &S0::v0, [](auto *v) { + ++*v; + return v; + }); + EXPECT_EQ(s1.s0.v0, 1); +} + +} // namespace P4::Test