reduce: remove unreferenced blocks pass (#2398)
diff --git a/source/reduce/CMakeLists.txt b/source/reduce/CMakeLists.txt
index 1a6ead4..4680231 100644
--- a/source/reduce/CMakeLists.txt
+++ b/source/reduce/CMakeLists.txt
@@ -24,6 +24,8 @@
reduction_opportunity_finder.h
reduction_pass.h
reduction_util.h
+ remove_block_reduction_opportunity.h
+ remove_block_reduction_opportunity_finder.h
remove_instruction_reduction_opportunity.h
remove_function_reduction_opportunity.h
remove_function_reduction_opportunity_finder.h
@@ -43,6 +45,8 @@
reduction_opportunity.cpp
reduction_pass.cpp
reduction_util.cpp
+ remove_block_reduction_opportunity.cpp
+ remove_block_reduction_opportunity_finder.cpp
remove_function_reduction_opportunity.cpp
remove_function_reduction_opportunity_finder.cpp
remove_instruction_reduction_opportunity.cpp
diff --git a/source/reduce/remove_block_reduction_opportunity.cpp b/source/reduce/remove_block_reduction_opportunity.cpp
new file mode 100644
index 0000000..9b952c4
--- /dev/null
+++ b/source/reduce/remove_block_reduction_opportunity.cpp
@@ -0,0 +1,56 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "remove_block_reduction_opportunity.h"
+
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace reduce {
+
+using namespace opt;
+
+RemoveBlockReductionOpportunity::RemoveBlockReductionOpportunity(
+ Function* function, BasicBlock* block)
+ : function_(function), block_(block) {
+ // precondition:
+ assert(block_->begin() != block_->end() &&
+ block_->begin()->context()->get_def_use_mgr()->NumUsers(
+ block_->id()) == 0 &&
+ "RemoveBlockReductionOpportunity block must have 0 references");
+}
+
+bool RemoveBlockReductionOpportunity::PreconditionHolds() {
+ // Removing other blocks cannot disable this opportunity.
+ return true;
+}
+
+void RemoveBlockReductionOpportunity::Apply() {
+ // We need an iterator pointing to the block, hence the loop.
+ for (auto bi = function_->begin(); bi != function_->end(); ++bi) {
+ if (bi->id() == block_->id()) {
+ bi->KillAllInsts(true);
+ bi.Erase();
+ // Block removal changes the function, but we don't use analyses, so no
+ // need to invalidate them.
+ return;
+ }
+ }
+
+ assert(false &&
+ "Unreachable: we should have found a block with the desired id.");
+}
+
+} // namespace reduce
+} // namespace spvtools
diff --git a/source/reduce/remove_block_reduction_opportunity.h b/source/reduce/remove_block_reduction_opportunity.h
new file mode 100644
index 0000000..45c0c17
--- /dev/null
+++ b/source/reduce/remove_block_reduction_opportunity.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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.
+
+#ifndef SOURCE_REDUCE_REMOVE_BLOCK_REDUCTION_OPPORTUNITY_H_
+#define SOURCE_REDUCE_REMOVE_BLOCK_REDUCTION_OPPORTUNITY_H_
+
+#include "reduction_opportunity.h"
+#include "source/opt/basic_block.h"
+#include "source/opt/function.h"
+
+namespace spvtools {
+namespace reduce {
+
+// An opportunity to remove an unreferenced block.
+// See RemoveBlockReductionOpportunityFinder.
+class RemoveBlockReductionOpportunity : public ReductionOpportunity {
+ public:
+ // Creates the opportunity to remove |block| in |function| in |context|.
+ RemoveBlockReductionOpportunity(opt::Function* function,
+ opt::BasicBlock* block);
+
+ bool PreconditionHolds() override;
+
+ protected:
+ void Apply() override;
+
+ private:
+ opt::Function* function_;
+ opt::BasicBlock* block_;
+};
+
+} // namespace reduce
+} // namespace spvtools
+
+#endif // SOURCE_REDUCE_REMOVE_BLOCK_REDUCTION_OPPORTUNITY_H_
diff --git a/source/reduce/remove_block_reduction_opportunity_finder.cpp b/source/reduce/remove_block_reduction_opportunity_finder.cpp
new file mode 100644
index 0000000..a4ca1f2
--- /dev/null
+++ b/source/reduce/remove_block_reduction_opportunity_finder.cpp
@@ -0,0 +1,96 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "source/reduce/remove_block_reduction_opportunity_finder.h"
+#include "source/reduce/remove_block_reduction_opportunity.h"
+
+namespace spvtools {
+namespace reduce {
+
+using namespace opt;
+
+std::string RemoveBlockReductionOpportunityFinder::GetName() const {
+ return "RemoveBlockReductionOpportunityFinder";
+}
+
+std::vector<std::unique_ptr<ReductionOpportunity>>
+RemoveBlockReductionOpportunityFinder::GetAvailableOpportunities(
+ opt::IRContext* context) const {
+ std::vector<std::unique_ptr<ReductionOpportunity>> result;
+
+ // Consider every block in every function.
+ for (auto& function : *context->module()) {
+ for (auto bi = function.begin(); bi != function.end(); ++bi) {
+ if (IsBlockValidOpportunity(context, function, bi)) {
+ result.push_back(spvtools::MakeUnique<RemoveBlockReductionOpportunity>(
+ &function, &*bi));
+ }
+ }
+ }
+ return result;
+}
+
+bool RemoveBlockReductionOpportunityFinder::IsBlockValidOpportunity(
+ opt::IRContext* context, opt::Function& function,
+ opt::Function::iterator& bi) {
+ assert(bi != function.end() && "Block iterator was out of bounds");
+
+ // Don't remove first block; we don't want to end up with no blocks.
+ if (bi == function.begin()) {
+ return false;
+ }
+
+ // Don't remove blocks with references.
+ if (context->get_def_use_mgr()->NumUsers(bi->id()) > 0) {
+ return false;
+ }
+
+ // Don't remove blocks whose instructions have outside references.
+ if (!BlockInstructionsHaveNoOutsideReferences(context, bi)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool RemoveBlockReductionOpportunityFinder::
+ BlockInstructionsHaveNoOutsideReferences(opt::IRContext* context,
+ const Function::iterator& bi) {
+ // Get all instructions in block.
+ std::unordered_set<uint32_t> instructions_in_block;
+ for (const Instruction& instruction : *bi) {
+ instructions_in_block.insert(instruction.unique_id());
+ }
+
+ // For each instruction...
+ for (const Instruction& instruction : *bi) {
+ // For each use of the instruction...
+ bool no_uses_outside_block = context->get_def_use_mgr()->WhileEachUser(
+ &instruction, [&instructions_in_block](Instruction* user) -> bool {
+ // If the use is in this block, continue (return true). Otherwise, we
+ // found an outside use; return false (and stop).
+ return instructions_in_block.find(user->unique_id()) !=
+ instructions_in_block.end();
+ });
+
+ if (!no_uses_outside_block) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace reduce
+} // namespace spvtools
diff --git a/source/reduce/remove_block_reduction_opportunity_finder.h b/source/reduce/remove_block_reduction_opportunity_finder.h
new file mode 100644
index 0000000..83cd04b
--- /dev/null
+++ b/source/reduce/remove_block_reduction_opportunity_finder.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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.
+
+#ifndef SOURCE_REDUCE_REMOVE_BLOCK_REDUCTION_OPPORTUNITY_FINDER_H_
+#define SOURCE_REDUCE_REMOVE_BLOCK_REDUCTION_OPPORTUNITY_FINDER_H_
+
+#include "source/opt/function.h"
+#include "source/reduce/reduction_opportunity_finder.h"
+
+namespace spvtools {
+namespace reduce {
+
+// A finder of opportunities to remove a block. The optimizer can remove dead
+// code. However, the reducer needs to be able to remove at a fine-grained
+// level.
+class RemoveBlockReductionOpportunityFinder
+ : public ReductionOpportunityFinder {
+ public:
+ RemoveBlockReductionOpportunityFinder() = default;
+
+ ~RemoveBlockReductionOpportunityFinder() override = default;
+
+ std::string GetName() const final;
+
+ std::vector<std::unique_ptr<ReductionOpportunity>> GetAvailableOpportunities(
+ opt::IRContext* context) const final;
+
+ private:
+ // Returns true if the block |bi| in function |function| is a valid
+ // opportunity according to various restrictions.
+ static bool IsBlockValidOpportunity(opt::IRContext* context,
+ opt::Function& function,
+ opt::Function::iterator& bi);
+
+ // Returns true if the instructions (definitions) in block |bi| have no
+ // references, except for references from inside the block itself.
+ static bool BlockInstructionsHaveNoOutsideReferences(
+ opt::IRContext* context, const opt::Function::iterator& bi);
+};
+
+} // namespace reduce
+} // namespace spvtools
+
+#endif // SOURCE_REDUCE_REMOVE_BLOCK_REDUCTION_OPPORTUNITY_FINDER_H_
diff --git a/test/reduce/CMakeLists.txt b/test/reduce/CMakeLists.txt
index df9542b..4acab56 100644
--- a/test/reduce/CMakeLists.txt
+++ b/test/reduce/CMakeLists.txt
@@ -20,6 +20,7 @@
reduce_test_util.cpp
reduce_test_util.h
reducer_test.cpp
+ remove_block_test.cpp
remove_function_test.cpp
remove_opname_instruction_test.cpp
remove_unreferenced_instruction_test.cpp
diff --git a/test/reduce/remove_block_test.cpp b/test/reduce/remove_block_test.cpp
new file mode 100644
index 0000000..71508c8
--- /dev/null
+++ b/test/reduce/remove_block_test.cpp
@@ -0,0 +1,358 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "reduce_test_util.h"
+#include "source/opt/build_module.h"
+#include "source/reduce/reduction_opportunity.h"
+#include "source/reduce/remove_block_reduction_opportunity.h"
+#include "source/reduce/remove_block_reduction_opportunity_finder.h"
+
+namespace spvtools {
+namespace reduce {
+namespace {
+
+TEST(RemoveBlockReductionPassTest, BasicCheck) {
+ std::string shader = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %4 "main"
+ OpExecutionMode %4 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %4 "main"
+ OpName %8 "x"
+ %2 = OpTypeVoid
+ %3 = OpTypeFunction %2
+ %6 = OpTypeInt 32 1
+ %7 = OpTypePointer Function %6
+ %9 = OpConstant %6 1
+ %10 = OpConstant %6 2
+ %11 = OpConstant %6 3
+ %12 = OpConstant %6 4
+ %4 = OpFunction %2 None %3
+ %5 = OpLabel
+ %8 = OpVariable %7 Function
+ OpBranch %14
+ %13 = OpLabel ; unreachable
+ OpStore %8 %9
+ OpBranch %14
+ %14 = OpLabel
+ OpStore %8 %10
+ OpBranch %16
+ %15 = OpLabel ; unreachable
+ OpStore %8 %11
+ OpBranch %16
+ %16 = OpLabel
+ OpStore %8 %12
+ OpBranch %17
+ %17 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )";
+ const auto env = SPV_ENV_UNIVERSAL_1_3;
+ const auto consumer = nullptr;
+ const auto context =
+ BuildModule(env, consumer, shader, kReduceAssembleOption);
+ const auto ops =
+ RemoveBlockReductionOpportunityFinder().GetAvailableOpportunities(
+ context.get());
+ ASSERT_EQ(2, ops.size());
+
+ ASSERT_TRUE(ops[0]->PreconditionHolds());
+ ops[0]->TryToApply();
+
+ std::string after_op_0 = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %4 "main"
+ OpExecutionMode %4 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %4 "main"
+ OpName %8 "x"
+ %2 = OpTypeVoid
+ %3 = OpTypeFunction %2
+ %6 = OpTypeInt 32 1
+ %7 = OpTypePointer Function %6
+ %9 = OpConstant %6 1
+ %10 = OpConstant %6 2
+ %11 = OpConstant %6 3
+ %12 = OpConstant %6 4
+ %4 = OpFunction %2 None %3
+ %5 = OpLabel
+ %8 = OpVariable %7 Function
+ OpBranch %14
+ %14 = OpLabel
+ OpStore %8 %10
+ OpBranch %16
+ %15 = OpLabel
+ OpStore %8 %11
+ OpBranch %16
+ %16 = OpLabel
+ OpStore %8 %12
+ OpBranch %17
+ %17 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )";
+
+ CheckEqual(env, after_op_0, context.get());
+
+ ASSERT_TRUE(ops[1]->PreconditionHolds());
+ ops[1]->TryToApply();
+
+ std::string after_op_1 = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %4 "main"
+ OpExecutionMode %4 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %4 "main"
+ OpName %8 "x"
+ %2 = OpTypeVoid
+ %3 = OpTypeFunction %2
+ %6 = OpTypeInt 32 1
+ %7 = OpTypePointer Function %6
+ %9 = OpConstant %6 1
+ %10 = OpConstant %6 2
+ %11 = OpConstant %6 3
+ %12 = OpConstant %6 4
+ %4 = OpFunction %2 None %3
+ %5 = OpLabel
+ %8 = OpVariable %7 Function
+ OpBranch %14
+ %14 = OpLabel
+ OpStore %8 %10
+ OpBranch %16
+ %16 = OpLabel
+ OpStore %8 %12
+ OpBranch %17
+ %17 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )";
+
+ CheckEqual(env, after_op_1, context.get());
+}
+
+TEST(RemoveBlockReductionPassTest, UnreachableContinueAndMerge) {
+ // Loop with unreachable merge and continue target. There should be no
+ // opportunities.
+
+ std::string shader = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %4 "main"
+ OpExecutionMode %4 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %4 "main"
+ %2 = OpTypeVoid
+ %3 = OpTypeFunction %2
+ %4 = OpFunction %2 None %3
+ %5 = OpLabel
+ OpBranch %13
+ %13 = OpLabel
+ OpLoopMerge %16 %15 None
+ OpBranch %14
+ %14 = OpLabel
+ OpReturn
+ %15 = OpLabel
+ OpBranch %13
+ %16 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )";
+ const auto env = SPV_ENV_UNIVERSAL_1_3;
+ const auto consumer = nullptr;
+ const auto context =
+ BuildModule(env, consumer, shader, kReduceAssembleOption);
+ const auto ops =
+ RemoveBlockReductionOpportunityFinder().GetAvailableOpportunities(
+ context.get());
+ ASSERT_EQ(0, ops.size());
+}
+
+TEST(RemoveBlockReductionPassTest, OneBlock) {
+ // Function with just one block. There should be no opportunities.
+
+ std::string shader = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %4 "main"
+ OpExecutionMode %4 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %4 "main"
+ %2 = OpTypeVoid
+ %3 = OpTypeFunction %2
+ %4 = OpFunction %2 None %3
+ %5 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )";
+ const auto env = SPV_ENV_UNIVERSAL_1_3;
+ const auto consumer = nullptr;
+ const auto context =
+ BuildModule(env, consumer, shader, kReduceAssembleOption);
+ const auto ops =
+ RemoveBlockReductionOpportunityFinder().GetAvailableOpportunities(
+ context.get());
+ ASSERT_EQ(0, ops.size());
+}
+
+TEST(RemoveBlockReductionPassTest, UnreachableBlocksWithOutsideIdUses) {
+ // A function with two unreachable blocks A -> B. A defines ID %9 and B uses
+ // %9. There are no references to A, but removing A would be invalid because
+ // of B's use of %9, so there should be no opportunities.
+
+ std::string shader = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %2 "main"
+ OpExecutionMode %2 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %2 "main"
+ %3 = OpTypeVoid
+ %4 = OpTypeInt 32 1
+ %5 = OpTypeFunction %3
+ %6 = OpConstant %4 1
+ %2 = OpFunction %3 None %5
+ %7 = OpLabel
+ OpReturn
+ %8 = OpLabel ; A
+ %9 = OpUndef %4
+ OpBranch %10
+ %10 = OpLabel ; B
+ %11 = OpIAdd %4 %6 %9 ; uses %9 from A, so A cannot be removed
+ OpReturn
+ OpFunctionEnd
+ )";
+ const auto env = SPV_ENV_UNIVERSAL_1_3;
+ const auto consumer = nullptr;
+ const auto context =
+ BuildModule(env, consumer, shader, kReduceAssembleOption);
+ const auto ops =
+ RemoveBlockReductionOpportunityFinder().GetAvailableOpportunities(
+ context.get());
+ ASSERT_EQ(0, ops.size());
+}
+
+TEST(RemoveBlockReductionPassTest, UnreachableBlocksWithInsideIdUses) {
+ // Similar to the above test.
+
+ // A function with two unreachable blocks A -> B. Both blocks create and use
+ // IDs, but the uses are contained within each block, so A should be removed.
+
+ std::string shader = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %2 "main"
+ OpExecutionMode %2 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %2 "main"
+ %3 = OpTypeVoid
+ %4 = OpTypeInt 32 1
+ %5 = OpTypeFunction %3
+ %6 = OpConstant %4 1
+ %2 = OpFunction %3 None %5
+ %7 = OpLabel
+ OpReturn
+ %8 = OpLabel ; A
+ %9 = OpUndef %4 ; define %9
+ %10 = OpIAdd %4 %6 %9 ; use %9
+ OpBranch %11
+ %11 = OpLabel ; B
+ %12 = OpUndef %4 ; define %12
+ %13 = OpIAdd %4 %6 %12 ; use %12
+ OpReturn
+ OpFunctionEnd
+ )";
+ const auto env = SPV_ENV_UNIVERSAL_1_3;
+ const auto consumer = nullptr;
+ const auto context =
+ BuildModule(env, consumer, shader, kReduceAssembleOption);
+ auto ops = RemoveBlockReductionOpportunityFinder().GetAvailableOpportunities(
+ context.get());
+ ASSERT_EQ(1, ops.size());
+
+ ASSERT_TRUE(ops[0]->PreconditionHolds());
+
+ ops[0]->TryToApply();
+
+ // Same as above, but block A is removed.
+ std::string after_op_0 = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %2 "main"
+ OpExecutionMode %2 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %2 "main"
+ %3 = OpTypeVoid
+ %4 = OpTypeInt 32 1
+ %5 = OpTypeFunction %3
+ %6 = OpConstant %4 1
+ %2 = OpFunction %3 None %5
+ %7 = OpLabel
+ OpReturn
+ %11 = OpLabel
+ %12 = OpUndef %4
+ %13 = OpIAdd %4 %6 %12
+ OpReturn
+ OpFunctionEnd
+ )";
+
+ CheckEqual(env, after_op_0, context.get());
+
+ // Find opportunities again. There are no reference to B. B should now be
+ // removed.
+
+ ops = RemoveBlockReductionOpportunityFinder().GetAvailableOpportunities(
+ context.get());
+
+ ASSERT_EQ(1, ops.size());
+
+ ASSERT_TRUE(ops[0]->PreconditionHolds());
+
+ ops[0]->TryToApply();
+
+ // Same as above, but block B is removed.
+ std::string after_op_0_again = R"(
+ OpCapability Shader
+ %1 = OpExtInstImport "GLSL.std.450"
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %2 "main"
+ OpExecutionMode %2 OriginUpperLeft
+ OpSource ESSL 310
+ OpName %2 "main"
+ %3 = OpTypeVoid
+ %4 = OpTypeInt 32 1
+ %5 = OpTypeFunction %3
+ %6 = OpConstant %4 1
+ %2 = OpFunction %3 None %5
+ %7 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )";
+
+ CheckEqual(env, after_op_0_again, context.get());
+}
+
+} // namespace
+} // namespace reduce
+} // namespace spvtools