spirv-reduce: Remove unused struct members (#3329)

Adds a reduction pass to remove unused members from structs.
diff --git a/source/reduce/CMakeLists.txt b/source/reduce/CMakeLists.txt
index b173ac0..d945bd2 100644
--- a/source/reduce/CMakeLists.txt
+++ b/source/reduce/CMakeLists.txt
@@ -31,7 +31,9 @@
         remove_instruction_reduction_opportunity.h
         remove_selection_reduction_opportunity.h
         remove_selection_reduction_opportunity_finder.h
+        remove_struct_member_reduction_opportunity.h
         remove_unused_instruction_reduction_opportunity_finder.h
+        remove_unused_struct_member_reduction_opportunity_finder.h
         structured_loop_to_selection_reduction_opportunity.h
         structured_loop_to_selection_reduction_opportunity_finder.h
         conditional_branch_to_simple_conditional_branch_opportunity_finder.h
@@ -57,7 +59,9 @@
         remove_instruction_reduction_opportunity.cpp
         remove_selection_reduction_opportunity.cpp
         remove_selection_reduction_opportunity_finder.cpp
+        remove_struct_member_reduction_opportunity.cpp
         remove_unused_instruction_reduction_opportunity_finder.cpp
+        remove_unused_struct_member_reduction_opportunity_finder.cpp
         structured_loop_to_selection_reduction_opportunity.cpp
         structured_loop_to_selection_reduction_opportunity_finder.cpp
         conditional_branch_to_simple_conditional_branch_opportunity_finder.cpp
diff --git a/source/reduce/reducer.cpp b/source/reduce/reducer.cpp
index 0179382..092d409 100644
--- a/source/reduce/reducer.cpp
+++ b/source/reduce/reducer.cpp
@@ -26,6 +26,7 @@
 #include "source/reduce/remove_function_reduction_opportunity_finder.h"
 #include "source/reduce/remove_selection_reduction_opportunity_finder.h"
 #include "source/reduce/remove_unused_instruction_reduction_opportunity_finder.h"
+#include "source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h"
 #include "source/reduce/simple_conditional_branch_to_branch_opportunity_finder.h"
 #include "source/reduce/structured_loop_to_selection_reduction_opportunity_finder.h"
 #include "source/spirv_reducer_options.h"
@@ -126,6 +127,8 @@
           ConditionalBranchToSimpleConditionalBranchOpportunityFinder>());
   AddReductionPass(
       spvtools::MakeUnique<SimpleConditionalBranchToBranchOpportunityFinder>());
+  AddReductionPass(spvtools::MakeUnique<
+                   RemoveUnusedStructMemberReductionOpportunityFinder>());
 
   // Cleanup passes.
 
diff --git a/source/reduce/remove_struct_member_reduction_opportunity.cpp b/source/reduce/remove_struct_member_reduction_opportunity.cpp
new file mode 100644
index 0000000..787c629
--- /dev/null
+++ b/source/reduce/remove_struct_member_reduction_opportunity.cpp
@@ -0,0 +1,208 @@
+// Copyright (c) 2020 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_struct_member_reduction_opportunity.h"
+
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace reduce {
+
+bool RemoveStructMemberReductionOpportunity::PreconditionHolds() {
+  return struct_type_->NumInOperands() == original_number_of_members_;
+}
+
+void RemoveStructMemberReductionOpportunity::Apply() {
+  std::set<opt::Instruction*> decorations_to_kill;
+
+  // We need to remove decorations that target the removed struct member, and
+  // adapt decorations that target later struct members by decrementing the
+  // member identifier.  We also need to adapt composite construction
+  // instructions so that no id is provided for the member being removed.
+  //
+  // To do this, we consider every use of the struct type.
+  struct_type_->context()->get_def_use_mgr()->ForEachUse(
+      struct_type_, [this, &decorations_to_kill](opt::Instruction* user,
+                                                 uint32_t /*operand_index*/) {
+        switch (user->opcode()) {
+          case SpvOpCompositeConstruct:
+          case SpvOpConstantComposite:
+            // This use is constructing a composite of the struct type, so we
+            // must remove the id that was provided for the member we are
+            // removing.
+            user->RemoveInOperand(member_index_);
+            break;
+          case SpvOpMemberDecorate:
+            // This use is decorating a member of the struct.
+            if (user->GetSingleWordInOperand(1) == member_index_) {
+              // The member we are removing is being decorated, so we record
+              // that we need to get rid of the decoration.
+              decorations_to_kill.insert(user);
+            } else if (user->GetSingleWordInOperand(1) > member_index_) {
+              // A member beyond the one we are removing is being decorated, so
+              // we adjust the index that identifies the member.
+              user->SetInOperand(1, {user->GetSingleWordInOperand(1) - 1});
+            }
+            break;
+          default:
+            break;
+        }
+      });
+
+  // Get rid of all the decorations that were found to target the member being
+  // removed.
+  for (auto decoration_to_kill : decorations_to_kill) {
+    decoration_to_kill->context()->KillInst(decoration_to_kill);
+  }
+
+  // We now look through all instructions that access composites via sequences
+  // of indices. Every time we find an index into the struct whose member is
+  // being removed, and if the member being accessed comes after the member
+  // being removed, we need to adjust the index accordingly.
+  //
+  // We go through every relevant instruction in every block of every function,
+  // and invoke a helper to adjust it.
+  auto context = struct_type_->context();
+  for (auto& function : *context->module()) {
+    for (auto& block : function) {
+      for (auto& inst : block) {
+        switch (inst.opcode()) {
+          case SpvOpAccessChain:
+          case SpvOpInBoundsAccessChain: {
+            // These access chain instructions take sequences of ids for
+            // indexing, starting from input operand 1.
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(context->get_def_use_mgr()
+                                 ->GetDef(inst.GetSingleWordInOperand(0))
+                                 ->type_id())
+                    ->GetSingleWordInOperand(1);
+            AdjustAccessedIndices(composite_type_id, 1, false, context, &inst);
+          } break;
+          case SpvOpPtrAccessChain:
+          case SpvOpInBoundsPtrAccessChain: {
+            // These access chain instructions take sequences of ids for
+            // indexing, starting from input operand 2.
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(context->get_def_use_mgr()
+                                 ->GetDef(inst.GetSingleWordInOperand(1))
+                                 ->type_id())
+                    ->GetSingleWordInOperand(1);
+            AdjustAccessedIndices(composite_type_id, 2, false, context, &inst);
+          } break;
+          case SpvOpCompositeExtract: {
+            // OpCompositeExtract uses literals for indexing, starting at input
+            // operand 1.
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(inst.GetSingleWordInOperand(0))
+                    ->type_id();
+            AdjustAccessedIndices(composite_type_id, 1, true, context, &inst);
+          } break;
+          case SpvOpCompositeInsert: {
+            // OpCompositeInsert uses literals for indexing, starting at input
+            // operand 2.
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(inst.GetSingleWordInOperand(1))
+                    ->type_id();
+            AdjustAccessedIndices(composite_type_id, 2, true, context, &inst);
+          } break;
+          default:
+            break;
+        }
+      }
+    }
+  }
+
+  // Remove the member from the struct type.
+  struct_type_->RemoveInOperand(member_index_);
+}
+
+void RemoveStructMemberReductionOpportunity::AdjustAccessedIndices(
+    uint32_t composite_type_id, uint32_t first_index_input_operand,
+    bool literal_indices, opt::IRContext* context,
+    opt::Instruction* composite_access_instruction) const {
+  // Walk the series of types that are encountered by following the
+  // instruction's sequence of indices. For all types except structs, this is
+  // routine: the type of the composite dictates what the next type will be
+  // regardless of the specific index value.
+  uint32_t next_type = composite_type_id;
+  for (uint32_t i = first_index_input_operand;
+       i < composite_access_instruction->NumInOperands(); i++) {
+    auto type_inst = context->get_def_use_mgr()->GetDef(next_type);
+    switch (type_inst->opcode()) {
+      case SpvOpTypeArray:
+      case SpvOpTypeMatrix:
+      case SpvOpTypeRuntimeArray:
+      case SpvOpTypeVector:
+        next_type = type_inst->GetSingleWordInOperand(0);
+        break;
+      case SpvOpTypeStruct: {
+        // Struct types are special becuase (a) we may need to adjust the index
+        // being used, if the struct type is the one from which we are removing
+        // a member, and (b) the type encountered by following the current index
+        // is dependent on the value of the index.
+
+        // Work out the member being accessed.  If literal indexing is used this
+        // is simple; otherwise we need to look up the id of the constant
+        // instruction being used as an index and get the value of the constant.
+        uint32_t index_operand =
+            composite_access_instruction->GetSingleWordInOperand(i);
+        uint32_t member = literal_indices ? index_operand
+                                          : context->get_def_use_mgr()
+                                                ->GetDef(index_operand)
+                                                ->GetSingleWordInOperand(0);
+
+        // The next type we will consider is obtained by looking up the struct
+        // type at |member|.
+        next_type = type_inst->GetSingleWordInOperand(member);
+
+        if (type_inst == struct_type_ && member > member_index_) {
+          // The struct type is the struct from which we are removing a member,
+          // and the member being accessed is beyond the member we are removing.
+          // We thus need to decrement the index by 1.
+          uint32_t new_in_operand;
+          if (literal_indices) {
+            // With literal indexing this is straightforward.
+            new_in_operand = member - 1;
+          } else {
+            // With id-based indexing this is more tricky: we need to find or
+            // create a constant instruction whose value is one less than
+            // |member|, and use the id of this constant as the replacement
+            // input operand.
+            auto constant_inst =
+                context->get_def_use_mgr()->GetDef(index_operand);
+            auto int_type = context->get_type_mgr()
+                                ->GetType(constant_inst->type_id())
+                                ->AsInteger();
+            auto new_index_constant =
+                opt::analysis::IntConstant(int_type, {member - 1});
+            new_in_operand = context->get_constant_mgr()
+                                 ->GetDefiningInstruction(&new_index_constant)
+                                 ->result_id();
+          }
+          composite_access_instruction->SetInOperand(i, {new_in_operand});
+        }
+      } break;
+      default:
+        assert(0 && "Unknown composite type.");
+        break;
+    }
+  }
+}
+
+}  // namespace reduce
+}  // namespace spvtools
diff --git a/source/reduce/remove_struct_member_reduction_opportunity.h b/source/reduce/remove_struct_member_reduction_opportunity.h
new file mode 100644
index 0000000..899e5ea
--- /dev/null
+++ b/source/reduce/remove_struct_member_reduction_opportunity.h
@@ -0,0 +1,84 @@
+// Copyright (c) 2020 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_STRUCT_MEMBER_REDUCTION_OPPORTUNITY_H_
+#define SOURCE_REDUCE_REMOVE_STRUCT_MEMBER_REDUCTION_OPPORTUNITY_H_
+
+#include "source/reduce/reduction_opportunity.h"
+
+#include "source/opt/instruction.h"
+
+namespace spvtools {
+namespace reduce {
+
+// An opportunity for removing a member from a struct type, adjusting all uses
+// of the struct accordingly.
+class RemoveStructMemberReductionOpportunity : public ReductionOpportunity {
+ public:
+  // Constructs a reduction opportunity from the struct type |struct_type|, for
+  // removal of member |member_index|.
+  RemoveStructMemberReductionOpportunity(opt::Instruction* struct_type,
+                                         uint32_t member_index)
+      : struct_type_(struct_type),
+        member_index_(member_index),
+        original_number_of_members_(struct_type->NumInOperands()) {}
+
+  // Opportunities to remove fields from a common struct type mutually
+  // invalidate each other.  We guard against this by requiring that the struct
+  // still has the number of members it had when the opportunity was created.
+  bool PreconditionHolds() override;
+
+ protected:
+  void Apply() override;
+
+ private:
+  // |composite_access_instruction| is an instruction that accesses a composite
+  // id using either a series of literal indices (e.g. in the case of
+  // OpCompositeInsert) or a series of index ids (e.g. in the case of
+  // OpAccessChain).
+  //
+  // This function adjusts the indices that are used by
+  // |composite_access_instruction| to that whenever an index is accessing a
+  // member of |struct_type_|, it is decremented if the member is beyond
+  // |member_index_|, to account for the removal of the |member_index_|-th
+  // member.
+  //
+  // |composite_type_id| is the id of the composite type that the series of
+  // indices is to be applied to.
+  //
+  // |first_index_input_operand| specifies the first input operand that is an
+  // index.
+  //
+  // |literal_indices| specifies whether indices are given as literals (true),
+  // or as ids (false).
+  //
+  // If id-based indexing is used, this function will add a constant for
+  // |member_index_| - 1 to the module if needed.
+  void AdjustAccessedIndices(
+      uint32_t composite_type_id, uint32_t first_index_input_operand,
+      bool literal_indices, opt::IRContext* context,
+      opt::Instruction* composite_access_instruction) const;
+
+  // The struct type from which a member is to be removed.
+  opt::Instruction* struct_type_;
+
+  uint32_t member_index_;
+
+  uint32_t original_number_of_members_;
+};
+
+}  // namespace reduce
+}  // namespace spvtools
+
+#endif  //   SOURCE_REDUCE_REMOVE_STRUCT_MEMBER_REDUCTION_OPPORTUNITY_H_
diff --git a/source/reduce/remove_unused_struct_member_reduction_opportunity_finder.cpp b/source/reduce/remove_unused_struct_member_reduction_opportunity_finder.cpp
new file mode 100644
index 0000000..39ce47f
--- /dev/null
+++ b/source/reduce/remove_unused_struct_member_reduction_opportunity_finder.cpp
@@ -0,0 +1,193 @@
+// Copyright (c) 2020 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_unused_struct_member_reduction_opportunity_finder.h"
+
+#include <map>
+#include <set>
+
+#include "source/reduce/remove_struct_member_reduction_opportunity.h"
+
+namespace spvtools {
+namespace reduce {
+
+std::vector<std::unique_ptr<ReductionOpportunity>>
+RemoveUnusedStructMemberReductionOpportunityFinder::GetAvailableOpportunities(
+    opt::IRContext* context) const {
+  std::vector<std::unique_ptr<ReductionOpportunity>> result;
+
+  // We track those struct members that are never accessed.  We do this by
+  // associating a member index to all the structs that have this member index
+  // but do not use it.  This representation is designed to allow reduction
+  // opportunities to be provided in a useful manner, so that opportunities
+  // associated with the same struct are unlikely to be adjacent.
+  std::map<uint32_t, std::set<opt::Instruction*>> unused_member_to_structs;
+
+  // Consider every struct type in the module.
+  for (auto& type_or_value : context->types_values()) {
+    if (type_or_value.opcode() != SpvOpTypeStruct) {
+      continue;
+    }
+
+    // Initially, we assume that *every* member of the struct is unused.  We
+    // then refine this based on observed uses.
+    std::set<uint32_t> unused_members;
+    for (uint32_t i = 0; i < type_or_value.NumInOperands(); i++) {
+      unused_members.insert(i);
+    }
+
+    // A separate reduction pass deals with removal of names.  If a struct
+    // member is still named, we treat it as being used.
+    context->get_def_use_mgr()->ForEachUse(
+        &type_or_value,
+        [&unused_members](opt::Instruction* user, uint32_t /*operand_index*/) {
+          switch (user->opcode()) {
+            case SpvOpMemberName:
+              unused_members.erase(user->GetSingleWordInOperand(1));
+              break;
+            default:
+              break;
+          }
+        });
+
+    for (uint32_t member : unused_members) {
+      if (!unused_member_to_structs.count(member)) {
+        unused_member_to_structs.insert(
+            {member, std::set<opt::Instruction*>()});
+      }
+      unused_member_to_structs.at(member).insert(&type_or_value);
+    }
+  }
+
+  // We now go through every instruction that might index into a struct, and
+  // refine our tracking of which struct members are used based on the struct
+  // indexing we observe.  We cannot just go through all uses of a struct type
+  // because the type is not necessarily even referenced, e.g. when walking
+  // arrays of structs.
+  for (auto& function : *context->module()) {
+    for (auto& block : function) {
+      for (auto& inst : block) {
+        switch (inst.opcode()) {
+          // For each indexing operation we observe, we invoke a helper to
+          // remove from our map those struct indices that are found to be used.
+          // The way the helper is invoked depends on whether the instruction
+          // uses literal or id indices, and the offset into the instruction's
+          // input operands from which index operands are provided.
+          case SpvOpAccessChain:
+          case SpvOpInBoundsAccessChain: {
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(context->get_def_use_mgr()
+                                 ->GetDef(inst.GetSingleWordInOperand(0))
+                                 ->type_id())
+                    ->GetSingleWordInOperand(1);
+            MarkAccessedMembersAsUsed(context, composite_type_id, 1, false,
+                                      inst, &unused_member_to_structs);
+          } break;
+          case SpvOpPtrAccessChain:
+          case SpvOpInBoundsPtrAccessChain: {
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(context->get_def_use_mgr()
+                                 ->GetDef(inst.GetSingleWordInOperand(1))
+                                 ->type_id())
+                    ->GetSingleWordInOperand(1);
+            MarkAccessedMembersAsUsed(context, composite_type_id, 2, false,
+                                      inst, &unused_member_to_structs);
+          } break;
+          case SpvOpCompositeExtract: {
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(inst.GetSingleWordInOperand(0))
+                    ->type_id();
+            MarkAccessedMembersAsUsed(context, composite_type_id, 1, true, inst,
+                                      &unused_member_to_structs);
+          } break;
+          case SpvOpCompositeInsert: {
+            auto composite_type_id =
+                context->get_def_use_mgr()
+                    ->GetDef(inst.GetSingleWordInOperand(1))
+                    ->type_id();
+            MarkAccessedMembersAsUsed(context, composite_type_id, 2, true, inst,
+                                      &unused_member_to_structs);
+          } break;
+          default:
+            break;
+        }
+      }
+    }
+  }
+
+  // We now know those struct indices that are unsed, and we make a reduction
+  // opportunity for each of them. By mapping each relevant member index to the
+  // structs in which it is unsed, we will group all opportunities to remove
+  // member k of a struct (for some k) together.  This reduces the likelihood
+  // that opportunities to remove members from the same struct will be adjacent,
+  // which is good because such opportunities mutually disable one another.
+  for (auto& entry : unused_member_to_structs) {
+    for (auto struct_type : entry.second) {
+      result.push_back(MakeUnique<RemoveStructMemberReductionOpportunity>(
+          struct_type, entry.first));
+    }
+  }
+  return result;
+}
+
+void RemoveUnusedStructMemberReductionOpportunityFinder::
+    MarkAccessedMembersAsUsed(
+        opt::IRContext* context, uint32_t composite_type_id,
+        uint32_t first_index_in_operand, bool literal_indices,
+        const opt::Instruction& composite_access_instruction,
+        std::map<uint32_t, std::set<opt::Instruction*>>*
+            unused_member_to_structs) const {
+  uint32_t next_type = composite_type_id;
+  for (uint32_t i = first_index_in_operand;
+       i < composite_access_instruction.NumInOperands(); i++) {
+    auto type_inst = context->get_def_use_mgr()->GetDef(next_type);
+    switch (type_inst->opcode()) {
+      case SpvOpTypeArray:
+      case SpvOpTypeMatrix:
+      case SpvOpTypeRuntimeArray:
+      case SpvOpTypeVector:
+        next_type = type_inst->GetSingleWordInOperand(0);
+        break;
+      case SpvOpTypeStruct: {
+        uint32_t index_operand =
+            composite_access_instruction.GetSingleWordInOperand(i);
+        uint32_t member = literal_indices ? index_operand
+                                          : context->get_def_use_mgr()
+                                                ->GetDef(index_operand)
+                                                ->GetSingleWordInOperand(0);
+        // Remove the struct type from the struct types associated with this
+        // member index, but only if a set of struct types is known to be
+        // associated with this member index.
+        if (unused_member_to_structs->count(member)) {
+          unused_member_to_structs->at(member).erase(type_inst);
+        }
+        next_type = type_inst->GetSingleWordInOperand(member);
+      } break;
+      default:
+        assert(0 && "Unknown composite type.");
+        break;
+    }
+  }
+}
+
+std::string RemoveUnusedStructMemberReductionOpportunityFinder::GetName()
+    const {
+  return "RemoveUnusedStructMemberReductionOpportunityFinder";
+}
+
+}  // namespace reduce
+}  // namespace spvtools
diff --git a/source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h b/source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h
new file mode 100644
index 0000000..13f4017
--- /dev/null
+++ b/source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2018 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_UNUSED_STRUCT_MEMBER_REDUCTION_OPPORTUNITY_FINDER_H_
+#define SOURCE_REDUCE_REMOVE_UNUSED_STRUCT_MEMBER_REDUCTION_OPPORTUNITY_FINDER_H_
+
+#include "source/reduce/reduction_opportunity_finder.h"
+
+namespace spvtools {
+namespace reduce {
+
+// A finder for opportunities to remove struct members that are not explicitly
+// used by extract, insert or access chain instructions.
+class RemoveUnusedStructMemberReductionOpportunityFinder
+    : public ReductionOpportunityFinder {
+ public:
+  RemoveUnusedStructMemberReductionOpportunityFinder() = default;
+
+  ~RemoveUnusedStructMemberReductionOpportunityFinder() override = default;
+
+  std::string GetName() const final;
+
+  std::vector<std::unique_ptr<ReductionOpportunity>> GetAvailableOpportunities(
+      opt::IRContext* context) const final;
+
+ private:
+  // A helper method to update |unused_members_to_structs| by removing from it
+  // all struct member accesses that take place in
+  // |composite_access_instruction|.
+  //
+  // |composite_type_id| is the type of the root object indexed into by the
+  // instruction.
+  //
+  // |first_index_in_operand| provides indicates where in the input operands the
+  // sequence of indices begins.
+  //
+  // |literal_indices| indicates whether indices are literals (true) or ids
+  // (false).
+  void MarkAccessedMembersAsUsed(
+      opt::IRContext* context, uint32_t composite_type_id,
+      uint32_t first_index_in_operand, bool literal_indices,
+      const opt::Instruction& composite_access_instruction,
+      std::map<uint32_t, std::set<opt::Instruction*>>* unused_member_to_structs)
+      const;
+};
+
+}  // namespace reduce
+}  // namespace spvtools
+
+#endif  // SOURCE_REDUCE_REMOVE_UNUSED_STRUCT_MEMBER_REDUCTION_OPPORTUNITY_FINDER_H_
diff --git a/test/reduce/CMakeLists.txt b/test/reduce/CMakeLists.txt
index fc8aee1..652f0ab 100644
--- a/test/reduce/CMakeLists.txt
+++ b/test/reduce/CMakeLists.txt
@@ -25,6 +25,7 @@
         remove_function_test.cpp
         remove_selection_test.cpp
         remove_unused_instruction_test.cpp
+        remove_unused_struct_member_test.cpp
         structured_loop_to_selection_test.cpp
         validation_during_reduction_test.cpp
         conditional_branch_to_simple_conditional_branch_test.cpp
diff --git a/test/reduce/remove_unused_struct_member_test.cpp b/test/reduce/remove_unused_struct_member_test.cpp
new file mode 100644
index 0000000..402ef2d
--- /dev/null
+++ b/test/reduce/remove_unused_struct_member_test.cpp
@@ -0,0 +1,238 @@
+// Copyright (c) 2020 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_unused_struct_member_reduction_opportunity_finder.h"
+
+#include "source/opt/build_module.h"
+#include "source/reduce/reduction_opportunity.h"
+#include "test/reduce/reduce_test_util.h"
+
+namespace spvtools {
+namespace reduce {
+namespace {
+
+TEST(RemoveUnusedStructMemberTest, RemoveOneMember) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeStruct %6 %6
+          %8 = OpTypePointer Function %7
+         %50 = OpConstant %6 0
+         %10 = OpConstant %6 1
+         %11 = OpConstant %6 2
+         %12 = OpConstantComposite %7 %10 %11
+         %13 = OpConstant %6 4
+         %14 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+               OpStore %9 %12
+         %15 = OpAccessChain %14 %9 %10
+         %22 = OpInBoundsAccessChain %14 %9 %10
+         %20 = OpLoad %7 %9
+         %21 = OpCompositeExtract %6 %20 1
+         %23 = OpCompositeInsert %7 %10 %20 1
+               OpStore %15 %13
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context =
+      BuildModule(env, consumer, shader, kReduceAssembleOption);
+
+  auto ops = RemoveUnusedStructMemberReductionOpportunityFinder()
+                 .GetAvailableOpportunities(context.get());
+  ASSERT_EQ(1, ops.size());
+  ASSERT_TRUE(ops[0]->PreconditionHolds());
+  ops[0]->TryToApply();
+
+  CheckValid(env, context.get());
+
+  std::string expected = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeStruct %6
+          %8 = OpTypePointer Function %7
+         %50 = OpConstant %6 0
+         %10 = OpConstant %6 1
+         %11 = OpConstant %6 2
+         %12 = OpConstantComposite %7 %11
+         %13 = OpConstant %6 4
+         %14 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+               OpStore %9 %12
+         %15 = OpAccessChain %14 %9 %50
+         %22 = OpInBoundsAccessChain %14 %9 %50
+         %20 = OpLoad %7 %9
+         %21 = OpCompositeExtract %6 %20 0
+         %23 = OpCompositeInsert %7 %10 %20 0
+               OpStore %15 %13
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  CheckEqual(env, expected, context.get());
+}
+
+TEST(RemoveUnusedStructMemberTest, RemoveUniformBufferMember) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpMemberDecorate %10 0 Offset 0
+               OpMemberDecorate %10 1 Offset 4
+               OpDecorate %10 Block
+               OpDecorate %12 DescriptorSet 0
+               OpDecorate %12 Binding 0
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypePointer Function %6
+          %9 = OpTypeInt 32 1
+         %10 = OpTypeStruct %9 %6
+         %11 = OpTypePointer Uniform %10
+         %12 = OpVariable %11 Uniform
+         %13 = OpConstant %9 1
+         %20 = OpConstant %9 0
+         %14 = OpTypePointer Uniform %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %15 = OpAccessChain %14 %12 %13
+         %16 = OpLoad %6 %15
+               OpStore %8 %16
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context =
+      BuildModule(env, consumer, shader, kReduceAssembleOption);
+
+  auto ops = RemoveUnusedStructMemberReductionOpportunityFinder()
+                 .GetAvailableOpportunities(context.get());
+  ASSERT_EQ(1, ops.size());
+  ASSERT_TRUE(ops[0]->PreconditionHolds());
+  ops[0]->TryToApply();
+
+  CheckValid(env, context.get());
+
+  std::string expected = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpMemberDecorate %10 0 Offset 4
+               OpDecorate %10 Block
+               OpDecorate %12 DescriptorSet 0
+               OpDecorate %12 Binding 0
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypePointer Function %6
+          %9 = OpTypeInt 32 1
+         %10 = OpTypeStruct %6
+         %11 = OpTypePointer Uniform %10
+         %12 = OpVariable %11 Uniform
+         %13 = OpConstant %9 1
+         %20 = OpConstant %9 0
+         %14 = OpTypePointer Uniform %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %15 = OpAccessChain %14 %12 %20
+         %16 = OpLoad %6 %15
+               OpStore %8 %16
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  CheckEqual(env, expected, context.get());
+}
+
+TEST(RemoveUnusedStructMemberTest, DoNotRemoveNamedMemberRemoveOneMember) {
+  // This illustrates that naming a member is enough to prevent its removal.
+  // Removal of names is done by a different pass.
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpMemberName %7 0 "someName"
+               OpMemberName %7 1 "someOtherName"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeStruct %6 %6
+          %8 = OpTypePointer Function %7
+         %50 = OpConstant %6 0
+         %10 = OpConstant %6 1
+         %11 = OpConstant %6 2
+         %12 = OpConstantComposite %7 %10 %11
+         %13 = OpConstant %6 4
+         %14 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+               OpStore %9 %12
+         %15 = OpAccessChain %14 %9 %10
+         %22 = OpInBoundsAccessChain %14 %9 %10
+         %20 = OpLoad %7 %9
+         %21 = OpCompositeExtract %6 %20 1
+         %23 = OpCompositeInsert %7 %10 %20 1
+               OpStore %15 %13
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context =
+      BuildModule(env, consumer, shader, kReduceAssembleOption);
+
+  auto ops = RemoveUnusedStructMemberReductionOpportunityFinder()
+                 .GetAvailableOpportunities(context.get());
+  ASSERT_EQ(0, ops.size());
+}
+
+}  // namespace
+}  // namespace reduce
+}  // namespace spvtools