spirv-fuzz: Fuzzer pass that adds access chains (#3182)

This change adds a fuzzer pass that sprinkles access chain
instructions into a module at random. This allows other passes to
have a richer set of pointers available to them, in particular the
passes that add loads and stores.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 330bbf0..4d5feea 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -37,6 +37,7 @@
         fuzzer.h
         fuzzer_context.h
         fuzzer_pass.h
+        fuzzer_pass_add_access_chains.h
         fuzzer_pass_add_composite_types.h
         fuzzer_pass_add_dead_blocks.h
         fuzzer_pass_add_dead_breaks.h
@@ -71,6 +72,7 @@
         replayer.h
         shrinker.h
         transformation.h
+        transformation_access_chain.h
         transformation_add_constant_boolean.h
         transformation_add_constant_composite.h
         transformation_add_constant_scalar.h
@@ -119,6 +121,7 @@
         fuzzer.cpp
         fuzzer_context.cpp
         fuzzer_pass.cpp
+        fuzzer_pass_add_access_chains.cpp
         fuzzer_pass_add_composite_types.cpp
         fuzzer_pass_add_dead_blocks.cpp
         fuzzer_pass_add_dead_breaks.cpp
@@ -152,6 +155,7 @@
         replayer.cpp
         shrinker.cpp
         transformation.cpp
+        transformation_access_chain.cpp
         transformation_add_constant_boolean.cpp
         transformation_add_constant_composite.cpp
         transformation_add_constant_scalar.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index a427619..6c2821c 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -21,6 +21,7 @@
 #include "fuzzer_pass_adjust_memory_operands_masks.h"
 #include "source/fuzz/fact_manager.h"
 #include "source/fuzz/fuzzer_context.h"
+#include "source/fuzz/fuzzer_pass_add_access_chains.h"
 #include "source/fuzz/fuzzer_pass_add_composite_types.h"
 #include "source/fuzz/fuzzer_pass_add_dead_blocks.h"
 #include "source/fuzz/fuzzer_pass_add_dead_breaks.h"
@@ -184,6 +185,9 @@
   // Apply some semantics-preserving passes.
   std::vector<std::unique_ptr<FuzzerPass>> passes;
   while (passes.empty()) {
+    MaybeAddPass<FuzzerPassAddAccessChains>(&passes, ir_context.get(),
+                                            &fact_manager, &fuzzer_context,
+                                            transformation_sequence_out);
     MaybeAddPass<FuzzerPassAddCompositeTypes>(&passes, ir_context.get(),
                                               &fact_manager, &fuzzer_context,
                                               transformation_sequence_out);
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index a434661..1265772 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -23,6 +23,7 @@
 // Default <minimum, maximum> pairs of probabilities for applying various
 // transformations. All values are percentages. Keep them in alphabetical order.
 
+const std::pair<uint32_t, uint32_t> kChanceOfAddingAccessChain = {5, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingAnotherStructField = {20,
                                                                          90};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingArrayOrStructType = {20, 90};
@@ -50,6 +51,8 @@
 const std::pair<uint32_t, uint32_t> kChanceOfConstructingComposite = {20, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfCopyingObject = {20, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfDonatingAdditionalModule = {5, 50};
+const std::pair<uint32_t, uint32_t> kChanceOfGoingDeeperWhenMakingAccessChain =
+    {50, 95};
 const std::pair<uint32_t, uint32_t> kChanceOfMakingDonorLivesafe = {40, 60};
 const std::pair<uint32_t, uint32_t> kChanceOfMergingBlocks = {20, 95};
 const std::pair<uint32_t, uint32_t> kChanceOfMovingBlockDown = {20, 50};
@@ -81,8 +84,14 @@
                              uint32_t min_fresh_id)
     : random_generator_(random_generator),
       next_fresh_id_(min_fresh_id),
+      max_loop_control_partial_count_(kDefaultMaxLoopControlPartialCount),
+      max_loop_control_peel_count_(kDefaultMaxLoopControlPeelCount),
+      max_loop_limit_(kDefaultMaxLoopLimit),
+      max_new_array_size_limit_(kDefaultMaxNewArraySizeLimit),
       go_deeper_in_constant_obfuscation_(
           kDefaultGoDeeperInConstantObfuscation) {
+  chance_of_adding_access_chain_ =
+      ChooseBetweenMinAndMax(kChanceOfAddingAccessChain);
   chance_of_adding_another_struct_field_ =
       ChooseBetweenMinAndMax(kChanceOfAddingAnotherStructField);
   chance_of_adding_array_or_struct_type_ =
@@ -122,6 +131,8 @@
   chance_of_copying_object_ = ChooseBetweenMinAndMax(kChanceOfCopyingObject);
   chance_of_donating_additional_module_ =
       ChooseBetweenMinAndMax(kChanceOfDonatingAdditionalModule);
+  chance_of_going_deeper_when_making_access_chain_ =
+      ChooseBetweenMinAndMax(kChanceOfGoingDeeperWhenMakingAccessChain);
   chance_of_making_donor_livesafe_ =
       ChooseBetweenMinAndMax(kChanceOfMakingDonorLivesafe);
   chance_of_merging_blocks_ = ChooseBetweenMinAndMax(kChanceOfMergingBlocks);
@@ -134,10 +145,6 @@
   chance_of_replacing_id_with_synonym_ =
       ChooseBetweenMinAndMax(kChanceOfReplacingIdWithSynonym);
   chance_of_splitting_block_ = ChooseBetweenMinAndMax(kChanceOfSplittingBlock);
-  max_loop_control_partial_count_ = kDefaultMaxLoopControlPartialCount;
-  max_loop_control_peel_count_ = kDefaultMaxLoopControlPeelCount;
-  max_loop_limit_ = kDefaultMaxLoopLimit;
-  max_new_array_size_limit_ = kDefaultMaxNewArraySizeLimit;
 }
 
 FuzzerContext::~FuzzerContext() = default;
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index 1d1245c..23127ff 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -68,6 +68,9 @@
 
   // Probabilities associated with applying various transformations.
   // Keep them in alphabetical order.
+  uint32_t GetChanceOfAddingAccessChain() {
+    return chance_of_adding_access_chain_;
+  }
   uint32_t GetChanceOfAddingAnotherStructField() {
     return chance_of_adding_another_struct_field_;
   }
@@ -119,6 +122,9 @@
   uint32_t GetChanceOfDonatingAdditionalModule() {
     return chance_of_donating_additional_module_;
   }
+  uint32_t GetChanceOfGoingDeeperWhenMakingAccessChain() {
+    return chance_of_going_deeper_when_making_access_chain_;
+  }
   uint32_t ChanceOfMakingDonorLivesafe() {
     return chance_of_making_donor_livesafe_;
   }
@@ -148,8 +154,11 @@
     return random_generator_->RandomUint32(max_new_array_size_limit_ - 1) + 1;
   }
 
-  // Functions to control how deeply to recurse.
-  // Keep them in alphabetical order.
+  // Other functions to control transformations. Keep them in alphabetical
+  // order.
+  uint32_t GetRandomIndexForAccessChain(uint32_t composite_size_bound) {
+    return random_generator_->RandomUint32(composite_size_bound);
+  }
   bool GoDeeperInConstantObfuscation(uint32_t depth) {
     return go_deeper_in_constant_obfuscation_(depth, random_generator_);
   }
@@ -162,6 +171,7 @@
 
   // Probabilities associated with applying various transformations.
   // Keep them in alphabetical order.
+  uint32_t chance_of_adding_access_chain_;
   uint32_t chance_of_adding_another_struct_field_;
   uint32_t chance_of_adding_array_or_struct_type_;
   uint32_t chance_of_adding_dead_block_;
@@ -183,6 +193,7 @@
   uint32_t chance_of_constructing_composite_;
   uint32_t chance_of_copying_object_;
   uint32_t chance_of_donating_additional_module_;
+  uint32_t chance_of_going_deeper_when_making_access_chain_;
   uint32_t chance_of_making_donor_livesafe_;
   uint32_t chance_of_merging_blocks_;
   uint32_t chance_of_moving_block_down_;
diff --git a/source/fuzz/fuzzer_pass.cpp b/source/fuzz/fuzzer_pass.cpp
index 1ecfa8d..4a22a21 100644
--- a/source/fuzz/fuzzer_pass.cpp
+++ b/source/fuzz/fuzzer_pass.cpp
@@ -219,21 +219,27 @@
   return result;
 }
 
-uint32_t FuzzerPass::FindOrCreatePointerTo32BitIntegerType(
-    bool is_signed, SpvStorageClass storage_class) {
-  auto uint32_type_id = FindOrCreate32BitIntegerType(is_signed);
-  opt::analysis::Pointer pointer_type(
-      GetIRContext()->get_type_mgr()->GetType(uint32_type_id), storage_class);
-  auto existing_id = GetIRContext()->get_type_mgr()->GetId(&pointer_type);
+uint32_t FuzzerPass::FindOrCreatePointerType(uint32_t base_type_id,
+                                             SpvStorageClass storage_class) {
+  // We do not use the type manager here, due to problems related to isomorphic
+  // but distinct structs not being regarded as different.
+  auto existing_id = fuzzerutil::MaybeGetPointerType(
+      GetIRContext(), base_type_id, storage_class);
   if (existing_id) {
     return existing_id;
   }
   auto result = GetFuzzerContext()->GetFreshId();
   ApplyTransformation(
-      TransformationAddTypePointer(result, storage_class, uint32_type_id));
+      TransformationAddTypePointer(result, storage_class, base_type_id));
   return result;
 }
 
+uint32_t FuzzerPass::FindOrCreatePointerTo32BitIntegerType(
+    bool is_signed, SpvStorageClass storage_class) {
+  return FindOrCreatePointerType(FindOrCreate32BitIntegerType(is_signed),
+                                 storage_class);
+}
+
 uint32_t FuzzerPass::FindOrCreate32BitIntegerConstant(uint32_t word,
                                                       bool is_signed) {
   auto uint32_type_id = FindOrCreate32BitIntegerType(is_signed);
diff --git a/source/fuzz/fuzzer_pass.h b/source/fuzz/fuzzer_pass.h
index 6853179..7052685 100644
--- a/source/fuzz/fuzzer_pass.h
+++ b/source/fuzz/fuzzer_pass.h
@@ -125,6 +125,12 @@
   // type itself do not exist, transformations are applied to add them.
   uint32_t FindOrCreateMatrixType(uint32_t column_count, uint32_t row_count);
 
+  // Returns the id of a pointer type with base type |base_type_id| (which must
+  // already exist) and storage class |storage_class|.  A transformation is
+  // applied to add the pointer if it does not already exist.
+  uint32_t FindOrCreatePointerType(uint32_t base_type_id,
+                                   SpvStorageClass storage_class);
+
   // Returns the id of an OpTypePointer instruction, with a 32-bit integer base
   // type of signedness specified by |is_signed|.  If the pointer type or
   // required integer base type do not exist, transformations are applied to add
diff --git a/source/fuzz/fuzzer_pass_add_access_chains.cpp b/source/fuzz/fuzzer_pass_add_access_chains.cpp
new file mode 100644
index 0000000..11f368e
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_add_access_chains.cpp
@@ -0,0 +1,169 @@
+// 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/fuzz/fuzzer_pass_add_access_chains.h"
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/transformation_access_chain.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassAddAccessChains::FuzzerPassAddAccessChains(
+    opt::IRContext* ir_context, FactManager* fact_manager,
+    FuzzerContext* fuzzer_context,
+    protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, fact_manager, fuzzer_context, transformations) {}
+
+FuzzerPassAddAccessChains::~FuzzerPassAddAccessChains() = default;
+
+void FuzzerPassAddAccessChains::Apply() {
+  MaybeAddTransformationBeforeEachInstruction(
+      [this](opt::Function* function, opt::BasicBlock* block,
+             opt::BasicBlock::iterator inst_it,
+             const protobufs::InstructionDescriptor& instruction_descriptor)
+          -> void {
+        assert(inst_it->opcode() ==
+                   instruction_descriptor.target_instruction_opcode() &&
+               "The opcode of the instruction we might insert before must be "
+               "the same as the opcode in the descriptor for the instruction");
+
+        // Check whether it is legitimate to insert an access chain
+        // instruction before this instruction.
+        if (!fuzzerutil::CanInsertOpcodeBeforeInstruction(SpvOpAccessChain,
+                                                          inst_it)) {
+          return;
+        }
+
+        // Randomly decide whether to try inserting a load here.
+        if (!GetFuzzerContext()->ChoosePercentage(
+                GetFuzzerContext()->GetChanceOfAddingAccessChain())) {
+          return;
+        }
+
+        // Get all of the pointers that are currently in scope, excluding
+        // explicitly null and undefined pointers.
+        std::vector<opt::Instruction*> relevant_pointer_instructions =
+            FindAvailableInstructions(
+                function, block, inst_it,
+                [](opt::IRContext* context,
+                   opt::Instruction* instruction) -> bool {
+                  if (!instruction->result_id() || !instruction->type_id()) {
+                    // A pointer needs both a result and type id.
+                    return false;
+                  }
+                  switch (instruction->opcode()) {
+                    case SpvOpConstantNull:
+                    case SpvOpUndef:
+                      // Do not allow making an access chain from a null or
+                      // undefined pointer.  (We can eliminate these cases
+                      // before actually checking that the instruction is a
+                      // pointer.)
+                      return false;
+                    default:
+                      break;
+                  }
+                  // If the instruction has pointer type, we can legitimately
+                  // make an access chain from it.
+                  return context->get_def_use_mgr()
+                             ->GetDef(instruction->type_id())
+                             ->opcode() == SpvOpTypePointer;
+                });
+
+        // At this point, |relevant_instructions| contains all the pointers
+        // we might think of making an access chain from.
+        if (relevant_pointer_instructions.empty()) {
+          return;
+        }
+
+        auto chosen_pointer =
+            relevant_pointer_instructions[GetFuzzerContext()->RandomIndex(
+                relevant_pointer_instructions)];
+        std::vector<uint32_t> index_ids;
+        auto pointer_type = GetIRContext()->get_def_use_mgr()->GetDef(
+            chosen_pointer->type_id());
+        uint32_t subobject_type_id = pointer_type->GetSingleWordInOperand(1);
+        while (true) {
+          auto subobject_type =
+              GetIRContext()->get_def_use_mgr()->GetDef(subobject_type_id);
+          if (!spvOpcodeIsComposite(subobject_type->opcode())) {
+            break;
+          }
+          if (!GetFuzzerContext()->ChoosePercentage(
+                  GetFuzzerContext()
+                      ->GetChanceOfGoingDeeperWhenMakingAccessChain())) {
+            break;
+          }
+          uint32_t bound;
+          switch (subobject_type->opcode()) {
+            case SpvOpTypeArray:
+              bound = fuzzerutil::GetArraySize(*subobject_type, GetIRContext());
+              break;
+            case SpvOpTypeMatrix:
+            case SpvOpTypeVector:
+              bound = subobject_type->GetSingleWordInOperand(1);
+              break;
+            case SpvOpTypeStruct:
+              bound = fuzzerutil::GetNumberOfStructMembers(*subobject_type);
+              break;
+            default:
+              assert(false && "Not a composite type opcode.");
+              // Set the bound to a value in order to keep release compilers
+              // happy.
+              bound = 0;
+              break;
+          }
+          if (bound == 0) {
+            // It is possible for a composite type to legitimately have zero
+            // sub-components, at least in the case of a struct, which
+            // can have no fields.
+            break;
+          }
+
+          // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3179) We
+          //  could allow non-constant indices when looking up non-structs,
+          //  using clamping to ensure they are in-bounds.
+          uint32_t index_value =
+              GetFuzzerContext()->GetRandomIndexForAccessChain(bound);
+          index_ids.push_back(FindOrCreate32BitIntegerConstant(
+              index_value, GetFuzzerContext()->ChooseEven()));
+          switch (subobject_type->opcode()) {
+            case SpvOpTypeArray:
+            case SpvOpTypeMatrix:
+            case SpvOpTypeVector:
+              subobject_type_id = subobject_type->GetSingleWordInOperand(0);
+              break;
+            case SpvOpTypeStruct:
+              subobject_type_id =
+                  subobject_type->GetSingleWordInOperand(index_value);
+              break;
+            default:
+              assert(false && "Not a composite type opcode.");
+          }
+        }
+        // The transformation we are about to create will only apply if a
+        // pointer suitable for the access chain's result type exists, so we
+        // create one if it does not.
+        FindOrCreatePointerType(subobject_type_id,
+                                static_cast<SpvStorageClass>(
+                                    pointer_type->GetSingleWordInOperand(0)));
+        // Apply the transformation to add an access chain.
+        ApplyTransformation(TransformationAccessChain(
+            GetFuzzerContext()->GetFreshId(), chosen_pointer->result_id(),
+            index_ids, instruction_descriptor));
+      });
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_add_access_chains.h b/source/fuzz/fuzzer_pass_add_access_chains.h
new file mode 100644
index 0000000..7e8ed61
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_add_access_chains.h
@@ -0,0 +1,41 @@
+// 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_FUZZ_FUZZER_PASS_ADD_ACCESS_CHAINS_H_
+#define SOURCE_FUZZ_FUZZER_PASS_ADD_ACCESS_CHAINS_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// Fuzzer pass that randomly adds access chains based on pointers available in
+// the module.  Other passes can use these access chains, e.g. by loading from
+// them.
+class FuzzerPassAddAccessChains : public FuzzerPass {
+ public:
+  FuzzerPassAddAccessChains(opt::IRContext* ir_context,
+                            FactManager* fact_manager,
+                            FuzzerContext* fuzzer_context,
+                            protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassAddAccessChains();
+
+  void Apply() override;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_ADD_ACCESS_CHAINS_H_
diff --git a/source/fuzz/fuzzer_util.cpp b/source/fuzz/fuzzer_util.cpp
index cb90143..26961c8 100644
--- a/source/fuzz/fuzzer_util.cpp
+++ b/source/fuzz/fuzzer_util.cpp
@@ -262,44 +262,48 @@
   return result;
 }
 
+uint32_t WalkOneCompositeTypeIndex(opt::IRContext* context,
+                                   uint32_t base_object_type_id,
+                                   uint32_t index) {
+  auto should_be_composite_type =
+      context->get_def_use_mgr()->GetDef(base_object_type_id);
+  assert(should_be_composite_type && "The type should exist.");
+  switch (should_be_composite_type->opcode()) {
+    case SpvOpTypeArray: {
+      auto array_length = GetArraySize(*should_be_composite_type, context);
+      if (array_length == 0 || index >= array_length) {
+        return 0;
+      }
+      return should_be_composite_type->GetSingleWordInOperand(0);
+    }
+    case SpvOpTypeMatrix:
+    case SpvOpTypeVector: {
+      auto count = should_be_composite_type->GetSingleWordInOperand(1);
+      if (index >= count) {
+        return 0;
+      }
+      return should_be_composite_type->GetSingleWordInOperand(0);
+    }
+    case SpvOpTypeStruct: {
+      if (index >= GetNumberOfStructMembers(*should_be_composite_type)) {
+        return 0;
+      }
+      return should_be_composite_type->GetSingleWordInOperand(index);
+    }
+    default:
+      return 0;
+  }
+}
+
 uint32_t WalkCompositeTypeIndices(
     opt::IRContext* context, uint32_t base_object_type_id,
     const google::protobuf::RepeatedField<google::protobuf::uint32>& indices) {
   uint32_t sub_object_type_id = base_object_type_id;
   for (auto index : indices) {
-    auto should_be_composite_type =
-        context->get_def_use_mgr()->GetDef(sub_object_type_id);
-    assert(should_be_composite_type && "The type should exist.");
-    switch (should_be_composite_type->opcode()) {
-      case SpvOpTypeArray: {
-        auto array_length = GetArraySize(*should_be_composite_type, context);
-        if (array_length == 0 || index >= array_length) {
-          return 0;
-        }
-        sub_object_type_id =
-            should_be_composite_type->GetSingleWordInOperand(0);
-        break;
-      }
-      case SpvOpTypeMatrix:
-      case SpvOpTypeVector: {
-        auto count = should_be_composite_type->GetSingleWordInOperand(1);
-        if (index >= count) {
-          return 0;
-        }
-        sub_object_type_id =
-            should_be_composite_type->GetSingleWordInOperand(0);
-        break;
-      }
-      case SpvOpTypeStruct: {
-        if (index >= GetNumberOfStructMembers(*should_be_composite_type)) {
-          return 0;
-        }
-        sub_object_type_id =
-            should_be_composite_type->GetSingleWordInOperand(index);
-        break;
-      }
-      default:
-        return 0;
+    sub_object_type_id =
+        WalkOneCompositeTypeIndex(context, sub_object_type_id, index);
+    if (!sub_object_type_id) {
+      return 0;
     }
   }
   return sub_object_type_id;
@@ -501,6 +505,23 @@
       context->get_def_use_mgr()->GetDef(pointer_type_id));
 }
 
+uint32_t MaybeGetPointerType(opt::IRContext* context, uint32_t pointee_type_id,
+                             SpvStorageClass storage_class) {
+  for (auto& inst : context->types_values()) {
+    switch (inst.opcode()) {
+      case SpvOpTypePointer:
+        if (inst.GetSingleWordInOperand(0) == storage_class &&
+            inst.GetSingleWordInOperand(1) == pointee_type_id) {
+          return inst.result_id();
+        }
+        break;
+      default:
+        break;
+    }
+  }
+  return 0;
+}
+
 }  // namespace fuzzerutil
 
 }  // namespace fuzz
diff --git a/source/fuzz/fuzzer_util.h b/source/fuzz/fuzzer_util.h
index 292cd9f..daa836c 100644
--- a/source/fuzz/fuzzer_util.h
+++ b/source/fuzz/fuzzer_util.h
@@ -98,6 +98,21 @@
 std::vector<uint32_t> RepeatedFieldToVector(
     const google::protobuf::RepeatedField<uint32_t>& repeated_field);
 
+// Given a type id, |base_object_type_id|, returns 0 if the type is not a
+// composite type or if |index| is too large to be used as an index into the
+// composite.  Otherwise returns the type id of the type associated with the
+// composite's index.
+//
+// Example: if |base_object_type_id| is 10, and we have:
+//
+// %10 = OpTypeStruct %3 %4 %5
+//
+// then 3 will be returned if |index| is 0, 5 if |index| is 2, and 0 if index
+// is 3 or larger.
+uint32_t WalkOneCompositeTypeIndex(opt::IRContext* context,
+                                   uint32_t base_object_type_id,
+                                   uint32_t index);
+
 // Given a type id, |base_object_type_id|, checks that the given sequence of
 // |indices| is suitable for indexing into this type.  Returns the id of the
 // type of the final sub-object reached via the indices if they are valid, and
@@ -181,6 +196,11 @@
 SpvStorageClass GetStorageClassFromPointerType(opt::IRContext* context,
                                                uint32_t pointer_type_id);
 
+// Returns the id of a pointer with pointee type |pointee_type_id| and storage
+// class |storage_class|, if it exists, and 0 otherwise.
+uint32_t MaybeGetPointerType(opt::IRContext* context, uint32_t pointee_type_id,
+                             SpvStorageClass storage_class);
+
 }  // namespace fuzzerutil
 
 }  // namespace fuzz
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index 8a931ba..9773b60 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -341,12 +341,32 @@
     TransformationLoad load = 36;
     TransformationStore store = 37;
     TransformationFunctionCall function_call = 38;
+    TransformationAccessChain access_chain = 39;
     // Add additional option using the next available number.
   }
 }
 
 // Keep transformation message types in alphabetical order:
 
+message TransformationAccessChain {
+
+  // Adds an access chain instruction based on a given pointer and indices.
+
+  // Result id for the access chain
+  uint32 fresh_id = 1;
+
+  // The pointer from which the access chain starts
+  uint32 pointer_id = 2;
+
+  // Zero or more access chain indices
+  repeated uint32 index_id = 3;
+
+  // A descriptor for an instruction in a block before which the new
+  // OpAccessChain instruction should be inserted
+  InstructionDescriptor instruction_to_insert_before = 4;
+
+}
+
 message TransformationAddConstantBoolean {
 
   // Supports adding the constants true and false to a module, which may be
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index d41a734..52fcfd7 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -17,6 +17,7 @@
 #include <cassert>
 
 #include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/transformation_access_chain.h"
 #include "source/fuzz/transformation_add_constant_boolean.h"
 #include "source/fuzz/transformation_add_constant_composite.h"
 #include "source/fuzz/transformation_add_constant_scalar.h"
@@ -65,6 +66,8 @@
 std::unique_ptr<Transformation> Transformation::FromMessage(
     const protobufs::Transformation& message) {
   switch (message.transformation_case()) {
+    case protobufs::Transformation::TransformationCase::kAccessChain:
+      return MakeUnique<TransformationAccessChain>(message.access_chain());
     case protobufs::Transformation::TransformationCase::kAddConstantBoolean:
       return MakeUnique<TransformationAddConstantBoolean>(
           message.add_constant_boolean());
diff --git a/source/fuzz/transformation_access_chain.cpp b/source/fuzz/transformation_access_chain.cpp
new file mode 100644
index 0000000..8c31006
--- /dev/null
+++ b/source/fuzz/transformation_access_chain.cpp
@@ -0,0 +1,215 @@
+// 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/fuzz/transformation_access_chain.h"
+
+#include <vector>
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/instruction_descriptor.h"
+
+namespace spvtools {
+namespace fuzz {
+
+TransformationAccessChain::TransformationAccessChain(
+    const spvtools::fuzz::protobufs::TransformationAccessChain& message)
+    : message_(message) {}
+
+TransformationAccessChain::TransformationAccessChain(
+    uint32_t fresh_id, uint32_t pointer_id,
+    const std::vector<uint32_t>& index_id,
+    const protobufs::InstructionDescriptor& instruction_to_insert_before) {
+  message_.set_fresh_id(fresh_id);
+  message_.set_pointer_id(pointer_id);
+  for (auto id : index_id) {
+    message_.add_index_id(id);
+  }
+  *message_.mutable_instruction_to_insert_before() =
+      instruction_to_insert_before;
+}
+
+bool TransformationAccessChain::IsApplicable(
+    opt::IRContext* context,
+    const spvtools::fuzz::FactManager& /*unused*/) const {
+  // The result id must be fresh
+  if (!fuzzerutil::IsFreshId(context, message_.fresh_id())) {
+    return false;
+  }
+  // The pointer id must exist and have a type.
+  auto pointer = context->get_def_use_mgr()->GetDef(message_.pointer_id());
+  if (!pointer || !pointer->type_id()) {
+    return false;
+  }
+  // The type must indeed be a pointer
+  auto pointer_type = context->get_def_use_mgr()->GetDef(pointer->type_id());
+  if (pointer_type->opcode() != SpvOpTypePointer) {
+    return false;
+  }
+
+  // The described instruction to insert before must exist and be a suitable
+  // point where an OpAccessChain instruction could be inserted.
+  auto instruction_to_insert_before =
+      FindInstruction(message_.instruction_to_insert_before(), context);
+  if (!instruction_to_insert_before) {
+    return false;
+  }
+  if (!fuzzerutil::CanInsertOpcodeBeforeInstruction(
+          SpvOpAccessChain, instruction_to_insert_before)) {
+    return false;
+  }
+
+  // Do not allow making an access chain from a null or undefined pointer, as
+  // we do not want to allow accessing such pointers.  This might be acceptable
+  // in dead blocks, but we conservatively avoid it.
+  switch (pointer->opcode()) {
+    case SpvOpConstantNull:
+    case SpvOpUndef:
+      // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3185): When
+      //  fuzzing for real we would like an 'assert(false)' here.  But we also
+      //  want to be able to write negative unit tests.
+      return false;
+    default:
+      break;
+  }
+
+  // The pointer on which the access chain is to be based needs to be available
+  // (according to dominance rules) at the insertion point.
+  if (!fuzzerutil::IdIsAvailableBeforeInstruction(
+          context, instruction_to_insert_before, message_.pointer_id())) {
+    return false;
+  }
+
+  // We now need to use the given indices to walk the type structure of the
+  // base type of the pointer, making sure that (a) the indices correspond to
+  // integers, and (b) these integer values are in-bounds.
+
+  // Start from the base type of the pointer.
+  uint32_t subobject_type_id = pointer_type->GetSingleWordInOperand(1);
+
+  // Consider the given index ids in turn.
+  for (auto index_id : message_.index_id()) {
+    // Try to get the integer value associated with this index is.  The first
+    // component of the result will be false if the id did not correspond to an
+    // integer.  Otherwise, the integer with which the id is associated is the
+    // second component.
+    std::pair<bool, uint32_t> maybe_index_value =
+        GetIndexValue(context, index_id);
+    if (!maybe_index_value.first) {
+      // There was no integer: this index is no good.
+      return false;
+    }
+    // Try to walk down the type using this index.  This will yield 0 if the
+    // type is not a composite or the index is out of bounds, and the id of
+    // the next type otherwise.
+    subobject_type_id = fuzzerutil::WalkOneCompositeTypeIndex(
+        context, subobject_type_id, maybe_index_value.second);
+    if (!subobject_type_id) {
+      // Either the type was not a composite (so that too many indices were
+      // provided), or the index was out of bounds.
+      return false;
+    }
+  }
+  // At this point, |subobject_type_id| is the type of the value targeted by
+  // the new access chain.  The result type of the access chain should be a
+  // pointer to this type, with the same storage class as for the original
+  // pointer.  Such a pointer type needs to exist in the module.
+  //
+  // We do not use the type manager to look up this type, due to problems
+  // associated with pointers to isomorphic structs being regarded as the same.
+  return fuzzerutil::MaybeGetPointerType(
+             context, subobject_type_id,
+             static_cast<SpvStorageClass>(
+                 pointer_type->GetSingleWordInOperand(0))) != 0;
+}
+
+void TransformationAccessChain::Apply(
+    opt::IRContext* context, spvtools::fuzz::FactManager* fact_manager) const {
+  // The operands to the access chain are the pointer followed by the indices.
+  // The result type of the access chain is determined by where the indices
+  // lead.  We thus push the pointer to a sequence of operands, and then follow
+  // the indices, pushing each to the operand list and tracking the type
+  // obtained by following it.  Ultimately this yields the type of the
+  // component reached by following all the indices, and the result type is
+  // a pointer to this component type.
+  opt::Instruction::OperandList operands;
+
+  // Add the pointer id itself.
+  operands.push_back({SPV_OPERAND_TYPE_ID, {message_.pointer_id()}});
+
+  // Start walking the indices, starting with the pointer's base type.
+  auto pointer_type = context->get_def_use_mgr()->GetDef(
+      context->get_def_use_mgr()->GetDef(message_.pointer_id())->type_id());
+  uint32_t subobject_type_id = pointer_type->GetSingleWordInOperand(1);
+
+  // Go through the index ids in turn.
+  for (auto index_id : message_.index_id()) {
+    // Add the index id to the operands.
+    operands.push_back({SPV_OPERAND_TYPE_ID, {index_id}});
+    // Get the integer value associated with the index id.
+    uint32_t index_value = GetIndexValue(context, index_id).second;
+    // Walk to the next type in the composite object using this index.
+    subobject_type_id = fuzzerutil::WalkOneCompositeTypeIndex(
+        context, subobject_type_id, index_value);
+  }
+  // The access chain's result type is a pointer to the composite component that
+  // was reached after following all indices.  The storage class is that of the
+  // original pointer.
+  uint32_t result_type = fuzzerutil::MaybeGetPointerType(
+      context, subobject_type_id,
+      static_cast<SpvStorageClass>(pointer_type->GetSingleWordInOperand(0)));
+
+  // Add the access chain instruction to the module, and update the module's id
+  // bound.
+  fuzzerutil::UpdateModuleIdBound(context, message_.fresh_id());
+  FindInstruction(message_.instruction_to_insert_before(), context)
+      ->InsertBefore(
+          MakeUnique<opt::Instruction>(context, SpvOpAccessChain, result_type,
+                                       message_.fresh_id(), operands));
+
+  // Conservatively invalidate all analyses.
+  context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+
+  // If the base pointer's pointee value was irrelevant, the same is true of the
+  // pointee value of the result of this access chain.
+  if (fact_manager->PointeeValueIsIrrelevant(message_.pointer_id())) {
+    fact_manager->AddFactValueOfPointeeIsIrrelevant(message_.fresh_id());
+  }
+}
+
+protobufs::Transformation TransformationAccessChain::ToMessage() const {
+  protobufs::Transformation result;
+  *result.mutable_access_chain() = message_;
+  return result;
+}
+
+std::pair<bool, uint32_t> TransformationAccessChain::GetIndexValue(
+    opt::IRContext* context, uint32_t index_id) const {
+  auto index_instruction = context->get_def_use_mgr()->GetDef(index_id);
+  if (!index_instruction || !spvOpcodeIsConstant(index_instruction->opcode())) {
+    // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3179) We could
+    //  allow non-constant indices when looking up non-structs, using clamping
+    //  to ensure they are in-bounds.
+    return {false, 0};
+  }
+  auto index_type =
+      context->get_def_use_mgr()->GetDef(index_instruction->type_id());
+  if (index_type->opcode() != SpvOpTypeInt ||
+      index_type->GetSingleWordInOperand(0) != 32) {
+    return {false, 0};
+  }
+  return {true, index_instruction->GetSingleWordInOperand(0)};
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_access_chain.h b/source/fuzz/transformation_access_chain.h
new file mode 100644
index 0000000..92d9e6a
--- /dev/null
+++ b/source/fuzz/transformation_access_chain.h
@@ -0,0 +1,80 @@
+// 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_FUZZ_TRANSFORMATION_ACCESS_CHAIN_H_
+#define SOURCE_FUZZ_TRANSFORMATION_ACCESS_CHAIN_H_
+
+#include <utility>
+
+#include "source/fuzz/fact_manager.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationAccessChain : public Transformation {
+ public:
+  explicit TransformationAccessChain(
+      const protobufs::TransformationAccessChain& message);
+
+  TransformationAccessChain(
+      uint32_t fresh_id, uint32_t pointer_id,
+      const std::vector<uint32_t>& index_id,
+      const protobufs::InstructionDescriptor& instruction_to_insert_before);
+
+  // - |message_.fresh_id| must be fresh
+  // - |message_.instruction_to_insert_before| must identify an instruction
+  //   before which it is legitimate to insert an OpAccessChain instruction
+  // - |message_.pointer_id| must be a result id with pointer type that is
+  //   available (according to dominance rules) at the insertion point.
+  // - The pointer must not be OpConstantNull or OpUndef
+  // - |message_.index_id| must be a sequence of ids of 32-bit integer constants
+  //   such that it is possible to walk the pointee type of
+  //   |message_.pointer_id| using these indices, remaining in-bounds.
+  // - If type t is the final type reached by walking these indices, the module
+  //   must include an instruction "OpTypePointer SC %t" where SC is the storage
+  //   class associated with |message_.pointer_id|
+  bool IsApplicable(opt::IRContext* context,
+                    const FactManager& fact_manager) const override;
+
+  // Adds an instruction of the form:
+  //   |message_.fresh_id| = OpAccessChain %ptr |message_.index_id|
+  // where %ptr is the result if of an instruction declaring a pointer to the
+  // type reached by walking the pointee type of |message_.pointer_id| using
+  // the indices in |message_.index_id|, and with the same storage class as
+  // |message_.pointer_id|.
+  //
+  // If |fact_manager| reports that |message_.pointer_id| has an irrelevant
+  // pointee value, then the fact that |message_.fresh_id| (the result of the
+  // access chain) also has an irrelevant pointee value is also recorded.
+  void Apply(opt::IRContext* context, FactManager* fact_manager) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+ private:
+  // Returns {false, 0} if |index_id| does not correspond to a 32-bit integer
+  // constant.  Otherwise, returns {true, value}, where value is the value of
+  // the 32-bit integer constant to which |index_id| corresponds.
+  std::pair<bool, uint32_t> GetIndexValue(opt::IRContext* context,
+                                          uint32_t index_id) const;
+
+  protobufs::TransformationAccessChain message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_ACCESS_CHAIN_H_
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index 4a423a9..4211ff2 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -24,6 +24,7 @@
           fuzzer_pass_add_useful_constructs_test.cpp
           fuzzer_pass_donate_modules_test.cpp
           instruction_descriptor_test.cpp
+          transformation_access_chain_test.cpp
           transformation_add_constant_boolean_test.cpp
           transformation_add_constant_composite_test.cpp
           transformation_add_constant_scalar_test.cpp
diff --git a/test/fuzz/transformation_access_chain_test.cpp b/test/fuzz/transformation_access_chain_test.cpp
new file mode 100644
index 0000000..516d371
--- /dev/null
+++ b/test/fuzz/transformation_access_chain_test.cpp
@@ -0,0 +1,448 @@
+// 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/fuzz/transformation_access_chain.h"
+#include "source/fuzz/instruction_descriptor.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationAccessChainTest, BasicTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %48 %54
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 2
+         %50 = OpTypeMatrix %7 2
+         %70 = OpTypePointer Function %7
+         %71 = OpTypePointer Function %50
+          %8 = OpTypeStruct %7 %6
+          %9 = OpTypePointer Function %8
+         %10 = OpTypeInt 32 1
+         %11 = OpTypePointer Function %10
+         %12 = OpTypeFunction %10 %9 %11
+         %17 = OpConstant %10 0
+         %18 = OpTypeInt 32 0
+         %19 = OpConstant %18 0
+         %20 = OpTypePointer Function %6
+         %99 = OpTypePointer Private %6
+         %29 = OpConstant %6 0
+         %30 = OpConstant %6 1
+         %31 = OpConstantComposite %7 %29 %30
+         %32 = OpConstant %6 2
+         %33 = OpConstantComposite %8 %31 %32
+         %35 = OpConstant %10 10
+         %51 = OpConstant %18 10
+         %80 = OpConstant %18 0
+         %81 = OpConstant %10 1
+         %82 = OpConstant %18 2
+         %83 = OpConstant %10 3
+         %84 = OpConstant %18 4
+         %85 = OpConstant %10 5
+         %52 = OpTypeArray %50 %51
+         %53 = OpTypePointer Private %52
+         %45 = OpUndef %9
+         %46 = OpConstantNull %9
+         %47 = OpTypePointer Private %8
+         %48 = OpVariable %47 Private
+         %54 = OpVariable %53 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %28 = OpVariable %9 Function
+         %34 = OpVariable %11 Function
+         %36 = OpVariable %9 Function
+         %38 = OpVariable %11 Function
+         %44 = OpCopyObject %9 %36
+               OpStore %28 %33
+               OpStore %34 %35
+         %37 = OpLoad %8 %28
+               OpStore %36 %37
+         %39 = OpLoad %10 %34
+               OpStore %38 %39
+         %40 = OpFunctionCall %10 %15 %36 %38
+         %41 = OpLoad %10 %34
+         %42 = OpIAdd %10 %41 %40
+               OpStore %34 %42
+               OpReturn
+               OpFunctionEnd
+         %15 = OpFunction %10 None %12
+         %13 = OpFunctionParameter %9
+         %14 = OpFunctionParameter %11
+         %16 = OpLabel
+         %21 = OpAccessChain %20 %13 %17 %19
+         %43 = OpCopyObject %9 %13
+         %22 = OpLoad %6 %21
+         %23 = OpConvertFToS %10 %22
+         %24 = OpLoad %10 %14
+         %25 = OpIAdd %10 %23 %24
+               OpReturnValue %25
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Types:
+  // Ptr | Pointee | Storage class | GLSL for pointee    | Ids of this type
+  // ----+---------+---------------+---------------------+------------------
+  //  9  |    8    | Function      | struct(vec2, float) | 28, 36, 44, 13, 43
+  // 11  |   10    | Function      | int                 | 34, 38, 14
+  // 20  |    6    | Function      | float               | -
+  // 99  |    6    | Private       | float               | -
+  // 53  |   52    | Private       | mat2x2[10]          | 54
+  // 47  |    8    | Private       | struct(vec2, float) | 48
+  // 70  |    7    | Function      | vec2                | -
+  // 71  |   59    | Function      | mat2x2              | -
+
+  // Indices 0-5 are in ids 80-85
+
+  FactManager fact_manager;
+  fact_manager.AddFactValueOfPointeeIsIrrelevant(54);
+
+  // Bad: id is not fresh
+  ASSERT_FALSE(TransformationAccessChain(
+                   43, 43, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer id does not exist
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 1000, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer id is not a type
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 5, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer id is not a pointer
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 23, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: index id does not exist
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 43, {1000}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: index id is not a constant
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 43, {24}, MakeInstructionDescriptor(25, SpvOpIAdd, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: too many indices
+  ASSERT_FALSE(
+      TransformationAccessChain(100, 43, {80, 80, 80},
+                                MakeInstructionDescriptor(24, SpvOpLoad, 0))
+          .IsApplicable(context.get(), fact_manager));
+
+  // Bad: index id is out of bounds
+  ASSERT_FALSE(
+      TransformationAccessChain(100, 43, {80, 83},
+                                MakeInstructionDescriptor(24, SpvOpLoad, 0))
+          .IsApplicable(context.get(), fact_manager));
+
+  // Bad: attempt to insert before variable
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 34, {}, MakeInstructionDescriptor(36, SpvOpVariable, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer not available
+  ASSERT_FALSE(
+      TransformationAccessChain(
+          100, 43, {80}, MakeInstructionDescriptor(21, SpvOpAccessChain, 0))
+          .IsApplicable(context.get(), fact_manager));
+
+  // Bad: instruction descriptor does not identify anything
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 43, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 100))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer is null
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 45, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer is undef
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 46, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Bad: pointer to result type does not exist
+  ASSERT_FALSE(TransformationAccessChain(
+                   100, 52, {0}, MakeInstructionDescriptor(24, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), fact_manager));
+
+  {
+    TransformationAccessChain transformation(
+        100, 43, {80}, MakeInstructionDescriptor(24, SpvOpLoad, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(100));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        101, 28, {81}, MakeInstructionDescriptor(42, SpvOpReturn, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(101));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        102, 36, {80, 81}, MakeInstructionDescriptor(37, SpvOpStore, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(102));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        103, 44, {}, MakeInstructionDescriptor(44, SpvOpStore, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(103));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        104, 13, {80}, MakeInstructionDescriptor(21, SpvOpAccessChain, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(104));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        105, 34, {}, MakeInstructionDescriptor(44, SpvOpStore, 1));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(105));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        106, 38, {}, MakeInstructionDescriptor(40, SpvOpFunctionCall, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(106));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        107, 14, {}, MakeInstructionDescriptor(24, SpvOpLoad, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(107));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        108, 54, {85, 81, 81}, MakeInstructionDescriptor(24, SpvOpLoad, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_TRUE(fact_manager.PointeeValueIsIrrelevant(108));
+  }
+
+  {
+    TransformationAccessChain transformation(
+        109, 48, {80, 80}, MakeInstructionDescriptor(24, SpvOpLoad, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(fact_manager.PointeeValueIsIrrelevant(109));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %48 %54
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 2
+         %50 = OpTypeMatrix %7 2
+         %70 = OpTypePointer Function %7
+         %71 = OpTypePointer Function %50
+          %8 = OpTypeStruct %7 %6
+          %9 = OpTypePointer Function %8
+         %10 = OpTypeInt 32 1
+         %11 = OpTypePointer Function %10
+         %12 = OpTypeFunction %10 %9 %11
+         %17 = OpConstant %10 0
+         %18 = OpTypeInt 32 0
+         %19 = OpConstant %18 0
+         %20 = OpTypePointer Function %6
+         %99 = OpTypePointer Private %6
+         %29 = OpConstant %6 0
+         %30 = OpConstant %6 1
+         %31 = OpConstantComposite %7 %29 %30
+         %32 = OpConstant %6 2
+         %33 = OpConstantComposite %8 %31 %32
+         %35 = OpConstant %10 10
+         %51 = OpConstant %18 10
+         %80 = OpConstant %18 0
+         %81 = OpConstant %10 1
+         %82 = OpConstant %18 2
+         %83 = OpConstant %10 3
+         %84 = OpConstant %18 4
+         %85 = OpConstant %10 5
+         %52 = OpTypeArray %50 %51
+         %53 = OpTypePointer Private %52
+         %45 = OpUndef %9
+         %46 = OpConstantNull %9
+         %47 = OpTypePointer Private %8
+         %48 = OpVariable %47 Private
+         %54 = OpVariable %53 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %28 = OpVariable %9 Function
+         %34 = OpVariable %11 Function
+         %36 = OpVariable %9 Function
+         %38 = OpVariable %11 Function
+         %44 = OpCopyObject %9 %36
+        %103 = OpAccessChain %9 %44
+               OpStore %28 %33
+        %105 = OpAccessChain %11 %34
+               OpStore %34 %35
+         %37 = OpLoad %8 %28
+        %102 = OpAccessChain %20 %36 %80 %81
+               OpStore %36 %37
+         %39 = OpLoad %10 %34
+               OpStore %38 %39
+        %106 = OpAccessChain %11 %38
+         %40 = OpFunctionCall %10 %15 %36 %38
+         %41 = OpLoad %10 %34
+         %42 = OpIAdd %10 %41 %40
+               OpStore %34 %42
+        %101 = OpAccessChain %20 %28 %81
+               OpReturn
+               OpFunctionEnd
+         %15 = OpFunction %10 None %12
+         %13 = OpFunctionParameter %9
+         %14 = OpFunctionParameter %11
+         %16 = OpLabel
+        %104 = OpAccessChain %70 %13 %80
+         %21 = OpAccessChain %20 %13 %17 %19
+         %43 = OpCopyObject %9 %13
+         %22 = OpLoad %6 %21
+         %23 = OpConvertFToS %10 %22
+        %100 = OpAccessChain %70 %43 %80
+        %107 = OpAccessChain %11 %14
+        %108 = OpAccessChain %99 %54 %85 %81 %81
+        %109 = OpAccessChain %99 %48 %80 %80
+         %24 = OpLoad %10 %14
+         %25 = OpIAdd %10 %23 %24
+               OpReturnValue %25
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationAccessChainTest, IsomorphicStructs) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %11 %12
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeStruct %6
+          %8 = OpTypePointer Private %7
+          %9 = OpTypeStruct %6
+         %10 = OpTypePointer Private %9
+         %11 = OpVariable %8 Private
+         %12 = OpVariable %10 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  {
+    TransformationAccessChain transformation(
+        100, 11, {}, MakeInstructionDescriptor(5, SpvOpReturn, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    TransformationAccessChain transformation(
+        101, 12, {}, MakeInstructionDescriptor(5, SpvOpReturn, 0));
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %11 %12
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeStruct %6
+          %8 = OpTypePointer Private %7
+          %9 = OpTypeStruct %6
+         %10 = OpTypePointer Private %9
+         %11 = OpVariable %8 Private
+         %12 = OpVariable %10 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+        %100 = OpAccessChain %8 %11
+        %101 = OpAccessChain %10 %12
+               OpReturn
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools