spirv-fuzz: Support memory instructions MoveInstructionDown (#3700)

Part of #3605.
diff --git a/source/fuzz/transformation_move_instruction_down.cpp b/source/fuzz/transformation_move_instruction_down.cpp
index 7e38354..7069d07 100644
--- a/source/fuzz/transformation_move_instruction_down.cpp
+++ b/source/fuzz/transformation_move_instruction_down.cpp
@@ -14,11 +14,28 @@
 
 #include "source/fuzz/transformation_move_instruction_down.h"
 
+#include "external/spirv-headers/include/spirv/unified1/GLSL.std.450.h"
 #include "source/fuzz/fuzzer_util.h"
 #include "source/fuzz/instruction_descriptor.h"
 
 namespace spvtools {
 namespace fuzz {
+namespace {
+
+const char* const kExtensionSetName = "GLSL.std.450";
+
+std::string GetExtensionSet(opt::IRContext* ir_context,
+                            const opt::Instruction& op_ext_inst) {
+  assert(op_ext_inst.opcode() == SpvOpExtInst && "Wrong opcode");
+
+  const auto* ext_inst_import = ir_context->get_def_use_mgr()->GetDef(
+      op_ext_inst.GetSingleWordInOperand(0));
+  assert(ext_inst_import && "Extension set is not imported");
+
+  return ext_inst_import->GetInOperand(0).AsString();
+}
+
+}  // namespace
 
 TransformationMoveInstructionDown::TransformationMoveInstructionDown(
     const protobufs::TransformationMoveInstructionDown& message)
@@ -30,7 +47,8 @@
 }
 
 bool TransformationMoveInstructionDown::IsApplicable(
-    opt::IRContext* ir_context, const TransformationContext& /*unused*/) const {
+    opt::IRContext* ir_context,
+    const TransformationContext& transformation_context) const {
   // |instruction| must be valid.
   auto* inst = FindInstruction(message_.instruction(), ir_context);
   if (!inst) {
@@ -38,7 +56,7 @@
   }
 
   // Instruction's opcode must be supported by this transformation.
-  if (!IsOpcodeSupported(inst->opcode())) {
+  if (!IsInstructionSupported(ir_context, *inst)) {
     return false;
   }
 
@@ -56,6 +74,20 @@
     return false;
   }
 
+  // We don't risk swapping a memory instruction with an unsupported one.
+  if (!IsSimpleInstruction(ir_context, *inst) &&
+      !IsInstructionSupported(ir_context, *successor_it)) {
+    return false;
+  }
+
+  // It must be safe to swap the instructions without changing the semantics of
+  // the module.
+  if (IsInstructionSupported(ir_context, *successor_it) &&
+      !CanSafelySwapInstructions(ir_context, *inst, *successor_it,
+                                 *transformation_context.GetFactManager())) {
+    return false;
+  }
+
   // Check that we can insert |instruction| after |inst_it|.
   auto successors_successor_it = ++inst_it;
   if (successors_successor_it == inst_block->end() ||
@@ -68,7 +100,7 @@
   if (inst->result_id()) {
     for (uint32_t i = 0; i < successor_it->NumInOperands(); ++i) {
       const auto& operand = successor_it->GetInOperand(i);
-      if (operand.type == SPV_OPERAND_TYPE_ID &&
+      if (spvIsInIdType(operand.type) &&
           operand.words[0] == inst->result_id()) {
         return false;
       }
@@ -99,17 +131,24 @@
   return result;
 }
 
-bool TransformationMoveInstructionDown::IsOpcodeSupported(SpvOp opcode) {
+bool TransformationMoveInstructionDown::IsInstructionSupported(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
   // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3605):
-  //  We only support "simple" instructions that work don't with memory.
-  //  We should extend this so that we support the ones that modify the memory
-  //  too.
-  switch (opcode) {
+  //  Add support for more instructions here.
+  return IsSimpleInstruction(ir_context, inst) ||
+         IsMemoryInstruction(ir_context, inst) || IsBarrierInstruction(inst);
+}
+
+bool TransformationMoveInstructionDown::IsSimpleInstruction(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
+  switch (inst.opcode()) {
     case SpvOpNop:
     case SpvOpUndef:
     case SpvOpAccessChain:
     case SpvOpInBoundsAccessChain:
-    case SpvOpArrayLength:
+      // OpAccessChain and OpInBoundsAccessChain are considered simple
+      // instructions since they result in a pointer to the object in memory,
+      // not the object itself.
     case SpvOpVectorExtractDynamic:
     case SpvOpVectorInsertDynamic:
     case SpvOpVectorShuffle:
@@ -207,13 +246,484 @@
     case SpvOpBitReverse:
     case SpvOpBitCount:
     case SpvOpCopyLogical:
-    case SpvOpPtrEqual:
-    case SpvOpPtrNotEqual:
+      return true;
+    case SpvOpExtInst: {
+      const auto* ext_inst_import =
+          ir_context->get_def_use_mgr()->GetDef(inst.GetSingleWordInOperand(0));
+
+      if (ext_inst_import->GetInOperand(0).AsString() != kExtensionSetName) {
+        return false;
+      }
+
+      switch (static_cast<GLSLstd450>(inst.GetSingleWordInOperand(1))) {
+        case GLSLstd450Round:
+        case GLSLstd450RoundEven:
+        case GLSLstd450Trunc:
+        case GLSLstd450FAbs:
+        case GLSLstd450SAbs:
+        case GLSLstd450FSign:
+        case GLSLstd450SSign:
+        case GLSLstd450Floor:
+        case GLSLstd450Ceil:
+        case GLSLstd450Fract:
+        case GLSLstd450Radians:
+        case GLSLstd450Degrees:
+        case GLSLstd450Sin:
+        case GLSLstd450Cos:
+        case GLSLstd450Tan:
+        case GLSLstd450Asin:
+        case GLSLstd450Acos:
+        case GLSLstd450Atan:
+        case GLSLstd450Sinh:
+        case GLSLstd450Cosh:
+        case GLSLstd450Tanh:
+        case GLSLstd450Asinh:
+        case GLSLstd450Acosh:
+        case GLSLstd450Atanh:
+        case GLSLstd450Atan2:
+        case GLSLstd450Pow:
+        case GLSLstd450Exp:
+        case GLSLstd450Log:
+        case GLSLstd450Exp2:
+        case GLSLstd450Log2:
+        case GLSLstd450Sqrt:
+        case GLSLstd450InverseSqrt:
+        case GLSLstd450Determinant:
+        case GLSLstd450MatrixInverse:
+        case GLSLstd450ModfStruct:
+        case GLSLstd450FMin:
+        case GLSLstd450UMin:
+        case GLSLstd450SMin:
+        case GLSLstd450FMax:
+        case GLSLstd450UMax:
+        case GLSLstd450SMax:
+        case GLSLstd450FClamp:
+        case GLSLstd450UClamp:
+        case GLSLstd450SClamp:
+        case GLSLstd450FMix:
+        case GLSLstd450IMix:
+        case GLSLstd450Step:
+        case GLSLstd450SmoothStep:
+        case GLSLstd450Fma:
+        case GLSLstd450FrexpStruct:
+        case GLSLstd450Ldexp:
+        case GLSLstd450PackSnorm4x8:
+        case GLSLstd450PackUnorm4x8:
+        case GLSLstd450PackSnorm2x16:
+        case GLSLstd450PackUnorm2x16:
+        case GLSLstd450PackHalf2x16:
+        case GLSLstd450PackDouble2x32:
+        case GLSLstd450UnpackSnorm2x16:
+        case GLSLstd450UnpackUnorm2x16:
+        case GLSLstd450UnpackHalf2x16:
+        case GLSLstd450UnpackSnorm4x8:
+        case GLSLstd450UnpackUnorm4x8:
+        case GLSLstd450UnpackDouble2x32:
+        case GLSLstd450Length:
+        case GLSLstd450Distance:
+        case GLSLstd450Cross:
+        case GLSLstd450Normalize:
+        case GLSLstd450FaceForward:
+        case GLSLstd450Reflect:
+        case GLSLstd450Refract:
+        case GLSLstd450FindILsb:
+        case GLSLstd450FindSMsb:
+        case GLSLstd450FindUMsb:
+        case GLSLstd450NMin:
+        case GLSLstd450NMax:
+        case GLSLstd450NClamp:
+          return true;
+        default:
+          return false;
+      }
+    }
+    default:
+      return false;
+  }
+}
+
+bool TransformationMoveInstructionDown::IsMemoryReadInstruction(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
+  switch (inst.opcode()) {
+      // Some simple instructions.
+    case SpvOpLoad:
+    case SpvOpCopyMemory:
+      // Image instructions.
+    case SpvOpImageSampleImplicitLod:
+    case SpvOpImageSampleExplicitLod:
+    case SpvOpImageSampleDrefImplicitLod:
+    case SpvOpImageSampleDrefExplicitLod:
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
+    case SpvOpImageFetch:
+    case SpvOpImageGather:
+    case SpvOpImageDrefGather:
+    case SpvOpImageRead:
+    case SpvOpImageSparseSampleImplicitLod:
+    case SpvOpImageSparseSampleExplicitLod:
+    case SpvOpImageSparseSampleDrefImplicitLod:
+    case SpvOpImageSparseSampleDrefExplicitLod:
+    case SpvOpImageSparseSampleProjImplicitLod:
+    case SpvOpImageSparseSampleProjExplicitLod:
+    case SpvOpImageSparseSampleProjDrefImplicitLod:
+    case SpvOpImageSparseSampleProjDrefExplicitLod:
+    case SpvOpImageSparseFetch:
+    case SpvOpImageSparseGather:
+    case SpvOpImageSparseDrefGather:
+    case SpvOpImageSparseRead:
+      // Atomic instructions.
+    case SpvOpAtomicLoad:
+    case SpvOpAtomicExchange:
+    case SpvOpAtomicCompareExchange:
+    case SpvOpAtomicCompareExchangeWeak:
+    case SpvOpAtomicIIncrement:
+    case SpvOpAtomicIDecrement:
+    case SpvOpAtomicIAdd:
+    case SpvOpAtomicISub:
+    case SpvOpAtomicSMin:
+    case SpvOpAtomicUMin:
+    case SpvOpAtomicSMax:
+    case SpvOpAtomicUMax:
+    case SpvOpAtomicAnd:
+    case SpvOpAtomicOr:
+    case SpvOpAtomicXor:
+    case SpvOpAtomicFlagTestAndSet:
+      return true;
+      // Extensions.
+    case SpvOpExtInst: {
+      if (GetExtensionSet(ir_context, inst) != kExtensionSetName) {
+        return false;
+      }
+
+      switch (static_cast<GLSLstd450>(inst.GetSingleWordInOperand(1))) {
+        case GLSLstd450InterpolateAtCentroid:
+        case GLSLstd450InterpolateAtOffset:
+        case GLSLstd450InterpolateAtSample:
+          return true;
+        default:
+          return false;
+      }
+    }
+    default:
+      return false;
+  }
+}
+
+uint32_t TransformationMoveInstructionDown::GetMemoryReadTarget(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
+  (void)ir_context;  // |ir_context| is only used in assertions.
+  assert(IsMemoryReadInstruction(ir_context, inst) &&
+         "|inst| is not a memory read instruction");
+
+  switch (inst.opcode()) {
+      // Simple instructions.
+    case SpvOpLoad:
+      // Image instructions.
+    case SpvOpImageSampleImplicitLod:
+    case SpvOpImageSampleExplicitLod:
+    case SpvOpImageSampleDrefImplicitLod:
+    case SpvOpImageSampleDrefExplicitLod:
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
+    case SpvOpImageFetch:
+    case SpvOpImageGather:
+    case SpvOpImageDrefGather:
+    case SpvOpImageRead:
+    case SpvOpImageSparseSampleImplicitLod:
+    case SpvOpImageSparseSampleExplicitLod:
+    case SpvOpImageSparseSampleDrefImplicitLod:
+    case SpvOpImageSparseSampleDrefExplicitLod:
+    case SpvOpImageSparseSampleProjImplicitLod:
+    case SpvOpImageSparseSampleProjExplicitLod:
+    case SpvOpImageSparseSampleProjDrefImplicitLod:
+    case SpvOpImageSparseSampleProjDrefExplicitLod:
+    case SpvOpImageSparseFetch:
+    case SpvOpImageSparseGather:
+    case SpvOpImageSparseDrefGather:
+    case SpvOpImageSparseRead:
+      // Atomic instructions.
+    case SpvOpAtomicLoad:
+    case SpvOpAtomicExchange:
+    case SpvOpAtomicCompareExchange:
+    case SpvOpAtomicCompareExchangeWeak:
+    case SpvOpAtomicIIncrement:
+    case SpvOpAtomicIDecrement:
+    case SpvOpAtomicIAdd:
+    case SpvOpAtomicISub:
+    case SpvOpAtomicSMin:
+    case SpvOpAtomicUMin:
+    case SpvOpAtomicSMax:
+    case SpvOpAtomicUMax:
+    case SpvOpAtomicAnd:
+    case SpvOpAtomicOr:
+    case SpvOpAtomicXor:
+    case SpvOpAtomicFlagTestAndSet:
+      return inst.GetSingleWordInOperand(0);
+    case SpvOpCopyMemory:
+      return inst.GetSingleWordInOperand(1);
+    case SpvOpExtInst: {
+      assert(GetExtensionSet(ir_context, inst) == kExtensionSetName &&
+             "Extension set is not supported");
+
+      switch (static_cast<GLSLstd450>(inst.GetSingleWordInOperand(1))) {
+        case GLSLstd450InterpolateAtCentroid:
+        case GLSLstd450InterpolateAtOffset:
+        case GLSLstd450InterpolateAtSample:
+          return inst.GetSingleWordInOperand(2);
+        default:
+          // This assertion will fail if not all memory read extension
+          // instructions are handled in the switch.
+          assert(false && "Not all memory opcodes are handled");
+          return 0;
+      }
+    }
+    default:
+      // This assertion will fail if not all memory read opcodes are handled in
+      // the switch.
+      assert(false && "Not all memory opcodes are handled");
+      return 0;
+  }
+}
+
+bool TransformationMoveInstructionDown::IsMemoryWriteInstruction(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
+  switch (inst.opcode()) {
+      // Simple Instructions.
+    case SpvOpStore:
+    case SpvOpCopyMemory:
+      // Image instructions.
+    case SpvOpImageWrite:
+      // Atomic instructions.
+    case SpvOpAtomicStore:
+    case SpvOpAtomicExchange:
+    case SpvOpAtomicCompareExchange:
+    case SpvOpAtomicCompareExchangeWeak:
+    case SpvOpAtomicIIncrement:
+    case SpvOpAtomicIDecrement:
+    case SpvOpAtomicIAdd:
+    case SpvOpAtomicISub:
+    case SpvOpAtomicSMin:
+    case SpvOpAtomicUMin:
+    case SpvOpAtomicSMax:
+    case SpvOpAtomicUMax:
+    case SpvOpAtomicAnd:
+    case SpvOpAtomicOr:
+    case SpvOpAtomicXor:
+    case SpvOpAtomicFlagTestAndSet:
+    case SpvOpAtomicFlagClear:
+      return true;
+      // Extensions.
+    case SpvOpExtInst: {
+      if (GetExtensionSet(ir_context, inst) != kExtensionSetName) {
+        return false;
+      }
+
+      auto extension = static_cast<GLSLstd450>(inst.GetSingleWordInOperand(1));
+      return extension == GLSLstd450Modf || extension == GLSLstd450Frexp;
+    }
+    default:
+      return false;
+  }
+}
+
+uint32_t TransformationMoveInstructionDown::GetMemoryWriteTarget(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
+  (void)ir_context;  // |ir_context| is only used in assertions.
+  assert(IsMemoryWriteInstruction(ir_context, inst) &&
+         "|inst| is not a memory write instruction");
+
+  switch (inst.opcode()) {
+    case SpvOpStore:
+    case SpvOpCopyMemory:
+    case SpvOpImageWrite:
+    case SpvOpAtomicStore:
+    case SpvOpAtomicExchange:
+    case SpvOpAtomicCompareExchange:
+    case SpvOpAtomicCompareExchangeWeak:
+    case SpvOpAtomicIIncrement:
+    case SpvOpAtomicIDecrement:
+    case SpvOpAtomicIAdd:
+    case SpvOpAtomicISub:
+    case SpvOpAtomicSMin:
+    case SpvOpAtomicUMin:
+    case SpvOpAtomicSMax:
+    case SpvOpAtomicUMax:
+    case SpvOpAtomicAnd:
+    case SpvOpAtomicOr:
+    case SpvOpAtomicXor:
+    case SpvOpAtomicFlagTestAndSet:
+    case SpvOpAtomicFlagClear:
+      return inst.GetSingleWordInOperand(0);
+    case SpvOpExtInst: {
+      assert(GetExtensionSet(ir_context, inst) == kExtensionSetName &&
+             "Extension set is not supported");
+
+      switch (static_cast<GLSLstd450>(inst.GetSingleWordInOperand(1))) {
+        case GLSLstd450Modf:
+        case GLSLstd450Frexp:
+          return inst.GetSingleWordInOperand(3);
+        default:
+          // This assertion will fail if not all memory write extension
+          // instructions are handled in the switch.
+          assert(false && "Not all opcodes are handled");
+          return 0;
+      }
+    }
+    default:
+      // This assertion will fail if not all memory write opcodes are handled in
+      // the switch.
+      assert(false && "Not all opcodes are handled");
+      return 0;
+  }
+}
+
+bool TransformationMoveInstructionDown::IsMemoryInstruction(
+    opt::IRContext* ir_context, const opt::Instruction& inst) {
+  return IsMemoryReadInstruction(ir_context, inst) ||
+         IsMemoryWriteInstruction(ir_context, inst);
+}
+
+bool TransformationMoveInstructionDown::IsBarrierInstruction(
+    const opt::Instruction& inst) {
+  switch (inst.opcode()) {
+    case SpvOpMemoryBarrier:
+    case SpvOpControlBarrier:
+    case SpvOpMemoryNamedBarrier:
       return true;
     default:
       return false;
   }
 }
 
+bool TransformationMoveInstructionDown::CanSafelySwapInstructions(
+    opt::IRContext* ir_context, const opt::Instruction& a,
+    const opt::Instruction& b, const FactManager& fact_manager) {
+  assert(IsInstructionSupported(ir_context, a) &&
+         IsInstructionSupported(ir_context, b) &&
+         "Both opcodes must be supported");
+
+  // One of opcodes is simple - we can swap them without any side-effects.
+  if (IsSimpleInstruction(ir_context, a) ||
+      IsSimpleInstruction(ir_context, b)) {
+    return true;
+  }
+
+  // Both parameters are either memory instruction or barriers.
+
+  // One of the opcodes is a barrier - can't swap them.
+  if (IsBarrierInstruction(a) || IsBarrierInstruction(b)) {
+    return false;
+  }
+
+  // Both parameters are memory instructions.
+
+  // Both parameters only read from memory - it's OK to swap them.
+  if (!IsMemoryWriteInstruction(ir_context, a) &&
+      !IsMemoryWriteInstruction(ir_context, b)) {
+    return true;
+  }
+
+  // At least one of parameters is a memory read instruction.
+
+  // In theory, we can swap two memory instructions, one of which reads
+  // from the memory, if the read target (the pointer the memory is read from)
+  // and the write target (the memory is written into):
+  // - point to different memory regions
+  // - point to the same region with irrelevant value
+  // - point to the same region and the region is not used anymore.
+  //
+  // However, we can't currently determine if two pointers point to two
+  // different memory regions. That being said, if two pointers are not
+  // synonymous, they still might point to the same memory region. For example:
+  //   %1 = OpVariable ...
+  //   %2 = OpAccessChain %1 0
+  //   %3 = OpAccessChain %1 0
+  // In this pseudo-code, %2 and %3 are not synonymous but point to the same
+  // memory location. This implies that we can't determine if some memory
+  // location is not used in the block.
+  //
+  // With this in mind, consider two cases (we will build a table for each one):
+  // - one instruction only reads from memory, the other one only writes to it.
+  //   S - both point to the same memory region.
+  //   D - both point to different memory regions.
+  //   0, 1, 2 - neither, one of or both of the memory regions are irrelevant.
+  //   |-| - can't swap; |+| - can swap.
+  //     | 0 | 1 | 2 |
+  //   S : -   +   +
+  //   D : +   +   +
+  // - both instructions write to memory. Notation is the same.
+  //     | 0 | 1 | 2 |
+  //   S : *   +   +
+  //   D : +   +   +
+  //   * - we can swap two instructions that write into the same non-irrelevant
+  //   memory region if the written value is the same.
+  //
+  // Note that we can't always distinguish between S and D. Also note that
+  // in case of S, if one of the instructions is marked with
+  // PointeeValueIsIrrelevant, then the pointee of the other one is irrelevant
+  // as well even if the instruction is not marked with that fact.
+  //
+  // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3723):
+  //  This procedure can be improved when we can determine if two pointers point
+  //  to different memory regions.
+
+  // From now on we will denote an instruction that:
+  // - only reads from memory - R
+  // - only writes into memory - W
+  // - reads and writes - RW
+  //
+  // Both |a| and |b| can be either W or RW at this point. Additionally, at most
+  // one of them can be R. The procedure below checks all possible combinations
+  // of R, W and RW according to the tables above. We conservatively assume that
+  // both |a| and |b| point to the same memory region.
+
+  auto memory_is_irrelevant = [ir_context, &fact_manager](uint32_t id) {
+    const auto* inst = ir_context->get_def_use_mgr()->GetDef(id);
+    if (!inst->type_id()) {
+      return false;
+    }
+
+    const auto* type = ir_context->get_type_mgr()->GetType(inst->type_id());
+    assert(type && "|id| has invalid type");
+
+    if (!type->AsPointer()) {
+      return false;
+    }
+
+    return fact_manager.PointeeValueIsIrrelevant(id);
+  };
+
+  if (IsMemoryWriteInstruction(ir_context, a) &&
+      IsMemoryWriteInstruction(ir_context, b) &&
+      (memory_is_irrelevant(GetMemoryWriteTarget(ir_context, a)) ||
+       memory_is_irrelevant(GetMemoryWriteTarget(ir_context, b)))) {
+    // We ignore the case when the written value is the same. This is because
+    // the written value might not be equal to any of the instruction's
+    // operands.
+    return true;
+  }
+
+  if (IsMemoryReadInstruction(ir_context, a) &&
+      IsMemoryWriteInstruction(ir_context, b) &&
+      !memory_is_irrelevant(GetMemoryReadTarget(ir_context, a)) &&
+      !memory_is_irrelevant(GetMemoryWriteTarget(ir_context, b))) {
+    return false;
+  }
+
+  if (IsMemoryWriteInstruction(ir_context, a) &&
+      IsMemoryReadInstruction(ir_context, b) &&
+      !memory_is_irrelevant(GetMemoryWriteTarget(ir_context, a)) &&
+      !memory_is_irrelevant(GetMemoryReadTarget(ir_context, b))) {
+    return false;
+  }
+
+  return IsMemoryReadInstruction(ir_context, a) ||
+         IsMemoryReadInstruction(ir_context, b);
+}
+
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/transformation_move_instruction_down.h b/source/fuzz/transformation_move_instruction_down.h
index 3f3c302..8b1e5ed 100644
--- a/source/fuzz/transformation_move_instruction_down.h
+++ b/source/fuzz/transformation_move_instruction_down.h
@@ -49,9 +49,52 @@
   protobufs::Transformation ToMessage() const override;
 
  private:
-  // Returns true if the |opcode| is supported by this transformation (i.e.
-  // we can move an instruction with this opcode).
-  static bool IsOpcodeSupported(SpvOp opcode);
+  // Returns true if the |inst| is supported by this transformation.
+  static bool IsInstructionSupported(opt::IRContext* ir_context,
+                                     const opt::Instruction& inst);
+
+  // Returns true if |inst| represents a "simple" instruction. That is, it
+  // neither reads from nor writes to the memory and is not a barrier.
+  static bool IsSimpleInstruction(opt::IRContext* ir_context,
+                                  const opt::Instruction& inst);
+
+  // Returns true if |inst| reads from memory.
+  static bool IsMemoryReadInstruction(opt::IRContext* ir_context,
+                                      const opt::Instruction& inst);
+
+  // Returns id being used by |inst| to read from. |inst| must be a memory read
+  // instruction (see IsMemoryReadInstruction). Returned id is not guaranteed to
+  // have pointer type.
+  static uint32_t GetMemoryReadTarget(opt::IRContext* ir_context,
+                                      const opt::Instruction& inst);
+
+  // Returns true if |inst| that writes to the memory.
+  static bool IsMemoryWriteInstruction(opt::IRContext* ir_context,
+                                       const opt::Instruction& inst);
+
+  // Returns id being used by |inst| to write into. |inst| must be a memory
+  // write instruction (see IsMemoryWriteInstruction). Returned id is not
+  // guaranteed to have pointer type.
+  static uint32_t GetMemoryWriteTarget(opt::IRContext* ir_context,
+                                       const opt::Instruction& inst);
+
+  // Returns true if |inst| either reads from or writes to the memory
+  // (see IsMemoryReadInstruction and IsMemoryWriteInstruction accordingly).
+  static bool IsMemoryInstruction(opt::IRContext* ir_context,
+                                  const opt::Instruction& inst);
+
+  // Returns true if |inst| is a barrier instruction.
+  static bool IsBarrierInstruction(const opt::Instruction& inst);
+
+  // Returns true if it is possible to swap |a| and |b| without changing the
+  // module's semantics. |a| and |b| are required to be supported instructions
+  // (see IsInstructionSupported). In particular, if either |a| or |b| are
+  // memory or barrier instructions, some checks are used to only say that they
+  // can be swapped if the swap is definitely semantics-preserving.
+  static bool CanSafelySwapInstructions(opt::IRContext* ir_context,
+                                        const opt::Instruction& a,
+                                        const opt::Instruction& b,
+                                        const FactManager& fact_manager);
 
   protobufs::TransformationMoveInstructionDown message_;
 };
diff --git a/test/fuzz/transformation_move_instruction_down_test.cpp b/test/fuzz/transformation_move_instruction_down_test.cpp
index 088938e..b49cfd2 100644
--- a/test/fuzz/transformation_move_instruction_down_test.cpp
+++ b/test/fuzz/transformation_move_instruction_down_test.cpp
@@ -50,10 +50,15 @@
          %18 = OpLabel
                OpBranch %19
          %19 = OpLabel
+         %42 = OpFunctionCall %2 %40
          %22 = OpIAdd %6 %15 %15
          %21 = OpIAdd %6 %15 %15
                OpReturn
                OpFunctionEnd
+         %40 = OpFunction %2 None %3
+         %41 = OpLabel
+               OpReturn
+               OpFunctionEnd
   )";
 
   const auto env = SPV_ENV_UNIVERSAL_1_3;
@@ -79,10 +84,7 @@
                    MakeInstructionDescriptor(12, SpvOpVariable, 0))
                    .IsApplicable(context.get(), transformation_context));
   ASSERT_FALSE(TransformationMoveInstructionDown(
-                   MakeInstructionDescriptor(11, SpvOpStore, 0))
-                   .IsApplicable(context.get(), transformation_context));
-  ASSERT_FALSE(TransformationMoveInstructionDown(
-                   MakeInstructionDescriptor(14, SpvOpLoad, 0))
+                   MakeInstructionDescriptor(42, SpvOpFunctionCall, 0))
                    .IsApplicable(context.get(), transformation_context));
 
   // Can't move the last instruction in the block.
@@ -151,15 +153,554 @@
          %18 = OpLabel
                OpBranch %19
          %19 = OpLabel
+         %42 = OpFunctionCall %2 %40
          %21 = OpIAdd %6 %15 %15
          %22 = OpIAdd %6 %15 %15
                OpReturn
                OpFunctionEnd
+         %40 = OpFunction %2 None %3
+         %41 = OpLabel
+               OpReturn
+               OpFunctionEnd
   )";
 
   ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
 }
 
+TEST(TransformationMoveInstructionDownTest, HandlesUnsupportedInstructions) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %4 "main"
+               OpExecutionMode %4 LocalSize 16 1 1
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpConstant %6 2
+         %20 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %21 = OpVariable %20 Function %7
+
+          ; can swap simple and not supported instructions
+          %8 = OpCopyObject %6 %7
+          %9 = OpFunctionCall %2 %12
+
+         ; cannot swap memory and not supported instruction
+         %22 = OpLoad %6 %21
+         %23 = OpFunctionCall %2 %12
+
+         ; cannot swap barrier and not supported instruction
+               OpMemoryBarrier %7 %7
+         %24 = OpFunctionCall %2 %12
+
+               OpReturn
+               OpFunctionEnd
+         %12 = OpFunction %2 None %3
+         %13 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(&fact_manager,
+                                               validator_options);
+
+  // Swap memory instruction with an unsupported one.
+  ASSERT_FALSE(TransformationMoveInstructionDown(
+                   MakeInstructionDescriptor(22, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Swap memory barrier with an unsupported one.
+  ASSERT_FALSE(TransformationMoveInstructionDown(
+                   MakeInstructionDescriptor(23, SpvOpMemoryBarrier, 0))
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Swap simple instruction with an unsupported one.
+  TransformationMoveInstructionDown transformation(
+      MakeInstructionDescriptor(8, SpvOpCopyObject, 0));
+  ASSERT_TRUE(
+      transformation.IsApplicable(context.get(), transformation_context));
+  transformation.Apply(context.get(), &transformation_context);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %4 "main"
+               OpExecutionMode %4 LocalSize 16 1 1
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpConstant %6 2
+         %20 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %21 = OpVariable %20 Function %7
+
+          ; can swap simple and not supported instructions
+          %9 = OpFunctionCall %2 %12
+          %8 = OpCopyObject %6 %7
+
+         ; cannot swap memory and not supported instruction
+         %22 = OpLoad %6 %21
+         %23 = OpFunctionCall %2 %12
+
+         ; cannot swap barrier and not supported instruction
+               OpMemoryBarrier %7 %7
+         %24 = OpFunctionCall %2 %12
+
+               OpReturn
+               OpFunctionEnd
+         %12 = OpFunction %2 None %3
+         %13 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationMoveInstructionDownTest, HandlesBarrierInstructions) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %4 "main"
+               OpExecutionMode %4 LocalSize 16 1 1
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpConstant %6 2
+         %20 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %21 = OpVariable %20 Function %7
+
+          ; cannot swap two barrier instructions
+               OpMemoryBarrier %7 %7
+               OpMemoryBarrier %7 %7
+
+         ; cannot swap barrier and memory instructions
+               OpMemoryBarrier %7 %7
+         %22 = OpLoad %6 %21
+               OpMemoryBarrier %7 %7
+
+         ; can swap barrier and simple instructions
+         %23 = OpCopyObject %6 %7
+               OpMemoryBarrier %7 %7
+
+               OpReturn
+               OpFunctionEnd
+         %12 = OpFunction %2 None %3
+         %13 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(&fact_manager,
+                                               validator_options);
+
+  // Swap two barrier instructions.
+  ASSERT_FALSE(TransformationMoveInstructionDown(
+                   MakeInstructionDescriptor(21, SpvOpMemoryBarrier, 0))
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Swap barrier and memory instructions.
+  ASSERT_FALSE(TransformationMoveInstructionDown(
+                   MakeInstructionDescriptor(21, SpvOpMemoryBarrier, 2))
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationMoveInstructionDown(
+                   MakeInstructionDescriptor(22, SpvOpLoad, 0))
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Swap barrier and simple instructions.
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(23, SpvOpCopyObject, 0));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(22, SpvOpMemoryBarrier, 1));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  ASSERT_TRUE(IsEqual(env, shader, context.get()));
+}
+
+TEST(TransformationMoveInstructionDownTest, HandlesSimpleInstructions) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %4 "main"
+               OpExecutionMode %4 LocalSize 16 1 1
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpConstant %6 2
+         %20 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %21 = OpVariable %20 Function %7
+
+         ; can swap simple and barrier instructions
+         %40 = OpCopyObject %6 %7
+               OpMemoryBarrier %7 %7
+
+         ; can swap simple and memory instructions
+         %41 = OpCopyObject %6 %7
+         %22 = OpLoad %6 %21
+
+         ; can swap two simple instructions
+         %23 = OpCopyObject %6 %7
+         %42 = OpCopyObject %6 %7
+
+               OpReturn
+               OpFunctionEnd
+         %12 = OpFunction %2 None %3
+         %13 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(&fact_manager,
+                                               validator_options);
+
+  // Swap simple and barrier instructions.
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(40, SpvOpCopyObject, 0));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(21, SpvOpMemoryBarrier, 0));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  // Swap simple and memory instructions.
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(41, SpvOpCopyObject, 0));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(22, SpvOpLoad, 0));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  // Swap two simple instructions.
+  {
+    TransformationMoveInstructionDown transformation(
+        MakeInstructionDescriptor(23, SpvOpCopyObject, 0));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %4 "main"
+               OpExecutionMode %4 LocalSize 16 1 1
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpConstant %6 2
+         %20 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %21 = OpVariable %20 Function %7
+
+         ; can swap simple and barrier instructions
+         %40 = OpCopyObject %6 %7
+               OpMemoryBarrier %7 %7
+
+         ; can swap simple and memory instructions
+         %41 = OpCopyObject %6 %7
+         %22 = OpLoad %6 %21
+
+         ; can swap two simple instructions
+         %42 = OpCopyObject %6 %7
+         %23 = OpCopyObject %6 %7
+
+               OpReturn
+               OpFunctionEnd
+         %12 = OpFunction %2 None %3
+         %13 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationMoveInstructionDownTest, HandlesMemoryInstructions) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %4 "main"
+               OpExecutionMode %4 LocalSize 16 1 1
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpConstant %6 2
+         %20 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %21 = OpVariable %20 Function %7
+         %22 = OpVariable %20 Function %7
+
+         ; swap R and R instructions
+         %23 = OpLoad %6 %21
+         %24 = OpLoad %6 %22
+
+         ; swap R and RW instructions
+
+           ; can't swap
+         %25 = OpLoad %6 %21
+               OpCopyMemory %21 %22
+
+           ; can swap
+         %26 = OpLoad %6 %21
+               OpCopyMemory %22 %21
+
+         %27 = OpLoad %6 %22
+               OpCopyMemory %21 %22
+
+         %28 = OpLoad %6 %22
+               OpCopyMemory %22 %21
+
+         ; swap R and W instructions
+
+           ; can't swap
+         %29 = OpLoad %6 %21
+               OpStore %21 %7
+
+           ; can swap
+         %30 = OpLoad %6 %22
+               OpStore %21 %7
+
+         %31 = OpLoad %6 %21
+               OpStore %22 %7
+
+         %32 = OpLoad %6 %22
+               OpStore %22 %7
+
+         ; swap RW and RW instructions
+
+           ; can't swap
+               OpCopyMemory %21 %21
+               OpCopyMemory %21 %21
+
+               OpCopyMemory %21 %22
+               OpCopyMemory %21 %21
+
+               OpCopyMemory %21 %21
+               OpCopyMemory %21 %22
+
+           ; can swap
+               OpCopyMemory %22 %21
+               OpCopyMemory %21 %22
+
+               OpCopyMemory %22 %21
+               OpCopyMemory %22 %21
+
+               OpCopyMemory %21 %22
+               OpCopyMemory %21 %22
+
+         ; swap RW and W instructions
+
+           ; can't swap
+               OpCopyMemory %21 %21
+               OpStore %21 %7
+
+               OpStore %21 %7
+               OpCopyMemory %21 %21
+
+           ; can swap
+               OpCopyMemory %22 %21
+               OpStore %21 %7
+
+               OpCopyMemory %21 %22
+               OpStore %21 %7
+
+               OpCopyMemory %21 %21
+               OpStore %22 %7
+
+         ; swap W and W instructions
+
+           ; can't swap
+               OpStore %21 %7
+               OpStore %21 %7
+
+           ; can swap
+               OpStore %22 %7
+               OpStore %21 %7
+
+               OpStore %22 %7
+               OpStore %22 %7
+
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(&fact_manager,
+                                               validator_options);
+
+  fact_manager.AddFactValueOfPointeeIsIrrelevant(22);
+
+  // Invalid swaps.
+
+  protobufs::InstructionDescriptor invalid_swaps[] = {
+      // R and RW
+      MakeInstructionDescriptor(25, SpvOpLoad, 0),
+
+      // R and W
+      MakeInstructionDescriptor(29, SpvOpLoad, 0),
+
+      // RW and RW
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 0),
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 2),
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 4),
+
+      // RW and W
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 12),
+      MakeInstructionDescriptor(32, SpvOpStore, 1),
+
+      // W and W
+      MakeInstructionDescriptor(32, SpvOpStore, 6),
+  };
+
+  for (const auto& descriptor : invalid_swaps) {
+    ASSERT_FALSE(TransformationMoveInstructionDown(descriptor)
+                     .IsApplicable(context.get(), transformation_context));
+  }
+
+  // Valid swaps.
+  protobufs::InstructionDescriptor valid_swaps[] = {
+      // R and R
+      MakeInstructionDescriptor(23, SpvOpLoad, 0),
+      MakeInstructionDescriptor(24, SpvOpLoad, 0),
+
+      // R and RW
+      MakeInstructionDescriptor(26, SpvOpLoad, 0),
+      MakeInstructionDescriptor(25, SpvOpCopyMemory, 1),
+
+      MakeInstructionDescriptor(27, SpvOpLoad, 0),
+      MakeInstructionDescriptor(26, SpvOpCopyMemory, 1),
+
+      MakeInstructionDescriptor(28, SpvOpLoad, 0),
+      MakeInstructionDescriptor(27, SpvOpCopyMemory, 1),
+
+      // R and W
+      MakeInstructionDescriptor(30, SpvOpLoad, 0),
+      MakeInstructionDescriptor(29, SpvOpStore, 1),
+
+      MakeInstructionDescriptor(31, SpvOpLoad, 0),
+      MakeInstructionDescriptor(30, SpvOpStore, 1),
+
+      MakeInstructionDescriptor(32, SpvOpLoad, 0),
+      MakeInstructionDescriptor(31, SpvOpStore, 1),
+
+      // RW and RW
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 6),
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 6),
+
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 8),
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 8),
+
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 10),
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 10),
+
+      // RW and W
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 14),
+      MakeInstructionDescriptor(32, SpvOpStore, 3),
+
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 15),
+      MakeInstructionDescriptor(32, SpvOpStore, 4),
+
+      MakeInstructionDescriptor(32, SpvOpCopyMemory, 16),
+      MakeInstructionDescriptor(32, SpvOpStore, 5),
+
+      // W and W
+      MakeInstructionDescriptor(32, SpvOpStore, 8),
+      MakeInstructionDescriptor(32, SpvOpStore, 8),
+
+      MakeInstructionDescriptor(32, SpvOpStore, 10),
+      MakeInstructionDescriptor(32, SpvOpStore, 10),
+  };
+
+  for (const auto& descriptor : valid_swaps) {
+    TransformationMoveInstructionDown transformation(descriptor);
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    transformation.Apply(context.get(), &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  ASSERT_TRUE(IsEqual(env, shader, context.get()));
+}
+
 }  // namespace
 }  // namespace fuzz
 }  // namespace spvtools