Add pass to inject code for robust-buffer-access semantics (#2771)


spirv-opt: Add --graphics-robust-access

Clamps access chain indices so they are always
in bounds.

Assumes:
- Logical addressing mode
- No runtime-array-descriptor-indexing
- No variable pointers

Adds stub code for clamping coordinate and samples
for OpImageTexelPointer.

Adds SinglePassRunAndFail optimizer test fixture.

Android.mk: add source/opt/graphics_robust_access_pass.cpp

Adds Constant::GetSignExtendedValue, Constant::GetZeroExtendedValue
diff --git a/Android.mk b/Android.mk
index 82d9776..8a507da 100644
--- a/Android.mk
+++ b/Android.mk
@@ -110,6 +110,7 @@
 		source/opt/freeze_spec_constant_value_pass.cpp \
 		source/opt/function.cpp \
 		source/opt/generate_webgpu_initializers_pass.cpp \
+		source/opt/graphics_robust_access_pass.cpp \
 		source/opt/if_conversion.cpp \
 		source/opt/inline_pass.cpp \
 		source/opt/inline_exhaustive_pass.cpp \
diff --git a/BUILD.gn b/BUILD.gn
index 70772b9..90d80e5 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -521,6 +521,8 @@
     "source/opt/function.h",
     "source/opt/generate_webgpu_initializers_pass.cpp",
     "source/opt/generate_webgpu_initializers_pass.h",
+    "source/opt/graphics_robust_access_pass.cpp",
+    "source/opt/graphics_robust_access_pass.h",
     "source/opt/if_conversion.cpp",
     "source/opt/if_conversion.h",
     "source/opt/inline_exhaustive_pass.cpp",
diff --git a/include/spirv-tools/optimizer.hpp b/include/spirv-tools/optimizer.hpp
index 4c668b4..a52dcd0 100644
--- a/include/spirv-tools/optimizer.hpp
+++ b/include/spirv-tools/optimizer.hpp
@@ -763,6 +763,27 @@
 // continue-targets to legalize for WebGPU.
 Optimizer::PassToken CreateSplitInvalidUnreachablePass();
 
+// Creates a graphics robust access pass.
+//
+// This pass injects code to clamp indexed accesses to buffers and internal
+// arrays, providing guarantees satisfying Vulkan's robustBufferAccess rules.
+//
+// TODO(dneto): Clamps coordinates and sample index for pointer calculations
+// into storage images (OpImageTexelPointer).  For an cube array image, it
+// assumes the maximum layer count times 6 is at most 0xffffffff.
+//
+// NOTE: This pass will fail with a message if:
+// - The module is not a Shader module.
+// - The module declares VariablePointers, VariablePointersStorageBuffer, or
+//   RuntimeDescriptorArrayEXT capabilities.
+// - The module uses an addressing model other than Logical
+// - Access chain indices are wider than 64 bits.
+// - Access chain index for a struct is not an OpConstant integer or is out
+//   of range. (The module is already invalid if that is the case.)
+// - TODO(dneto): The OpImageTexelPointer coordinate component is not 32-bits
+// wide.
+Optimizer::PassToken CreateGraphicsRobustAccessPass();
+
 }  // namespace spvtools
 
 #endif  // INCLUDE_SPIRV_TOOLS_OPTIMIZER_HPP_
diff --git a/source/opt/CMakeLists.txt b/source/opt/CMakeLists.txt
index 3e1280e..278f794 100644
--- a/source/opt/CMakeLists.txt
+++ b/source/opt/CMakeLists.txt
@@ -48,6 +48,7 @@
   freeze_spec_constant_value_pass.h
   function.h
   generate_webgpu_initializers_pass.h
+  graphics_robust_access_pass.h
   if_conversion.h
   inline_exhaustive_pass.h
   inline_opaque_pass.h
@@ -147,6 +148,7 @@
   fold_spec_constant_op_and_composite_pass.cpp
   freeze_spec_constant_value_pass.cpp
   function.cpp
+  graphics_robust_access_pass.cpp
   generate_webgpu_initializers_pass.cpp
   if_conversion.cpp
   inline_exhaustive_pass.cpp
diff --git a/source/opt/constants.cpp b/source/opt/constants.cpp
index 3c05f9e..b5875c9 100644
--- a/source/opt/constants.cpp
+++ b/source/opt/constants.cpp
@@ -103,6 +103,45 @@
   }
 }
 
+uint64_t Constant::GetZeroExtendedValue() const {
+  const auto* int_type = type()->AsInteger();
+  assert(int_type != nullptr);
+  const auto width = int_type->width();
+  assert(width <= 64);
+
+  uint64_t value = 0;
+  if (const IntConstant* ic = AsIntConstant()) {
+    if (width <= 32) {
+      value = ic->GetU32BitValue();
+    } else {
+      value = ic->GetU64BitValue();
+    }
+  } else {
+    assert(AsNullConstant() && "Must be an integer constant.");
+  }
+  return value;
+}
+
+int64_t Constant::GetSignExtendedValue() const {
+  const auto* int_type = type()->AsInteger();
+  assert(int_type != nullptr);
+  const auto width = int_type->width();
+  assert(width <= 64);
+
+  int64_t value = 0;
+  if (const IntConstant* ic = AsIntConstant()) {
+    if (width <= 32) {
+      // Let the C++ compiler do the sign extension.
+      value = int64_t(ic->GetS32BitValue());
+    } else {
+      value = ic->GetS64BitValue();
+    }
+  } else {
+    assert(AsNullConstant() && "Must be an integer constant.");
+  }
+  return value;
+}
+
 ConstantManager::ConstantManager(IRContext* ctx) : ctx_(ctx) {
   // Populate the constant table with values from constant declarations in the
   // module.  The values of each OpConstant declaration is the identity
diff --git a/source/opt/constants.h b/source/opt/constants.h
index a8e0fb5..7b9f248 100644
--- a/source/opt/constants.h
+++ b/source/opt/constants.h
@@ -116,6 +116,14 @@
   // Integer type.
   int64_t GetS64() const;
 
+  // Returns the zero-extended representation of an integer constant. Must
+  // be an integral constant of at most 64 bits.
+  uint64_t GetZeroExtendedValue() const;
+
+  // Returns the sign-extended representation of an integer constant. Must
+  // be an integral constant of at most 64 bits.
+  int64_t GetSignExtendedValue() const;
+
   // Returns true if the constant is a zero or a composite containing 0s.
   virtual bool IsZero() const { return false; }
 
diff --git a/source/opt/graphics_robust_access_pass.cpp b/source/opt/graphics_robust_access_pass.cpp
new file mode 100644
index 0000000..dd60e8c
--- /dev/null
+++ b/source/opt/graphics_robust_access_pass.cpp
@@ -0,0 +1,968 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This pass injects code in a graphics shader to implement guarantees
+// satisfying Vulkan's robustBufferAcces rules.  Robust access rules permit
+// an out-of-bounds access to be redirected to an access of the same type
+// (load, store, etc.) but within the same root object.
+//
+// We assume baseline functionality in Vulkan, i.e. the module uses
+// logical addressing mode, without VK_KHR_variable_pointers.
+//
+//    - Logical addressing mode implies:
+//      - Each root pointer (a pointer that exists other than by the
+//        execution of a shader instruction) is the result of an OpVariable.
+//
+//      - Instructions that result in pointers are:
+//          OpVariable
+//          OpAccessChain
+//          OpInBoundsAccessChain
+//          OpFunctionParameter
+//          OpImageTexelPointer
+//          OpCopyObject
+//
+//      - Instructions that use a pointer are:
+//          OpLoad
+//          OpStore
+//          OpAccessChain
+//          OpInBoundsAccessChain
+//          OpFunctionCall
+//          OpImageTexelPointer
+//          OpCopyMemory
+//          OpCopyObject
+//          all OpAtomic* instructions
+//
+// We classify pointer-users into:
+//  - Accesses:
+//    - OpLoad
+//    - OpStore
+//    - OpAtomic*
+//    - OpCopyMemory
+//
+//  - Address calculations:
+//    - OpAccessChain
+//    - OpInBoundsAccessChain
+//
+//  - Pass-through:
+//    - OpFunctionCall
+//    - OpFunctionParameter
+//    - OpCopyObject
+//
+// The strategy is:
+//
+//  - Handle only logical addressing mode. In particular, don't handle a module
+//    if it uses one of the variable-pointers capabilities.
+//
+//  - Don't handle modules using capability RuntimeDescriptorArrayEXT.  So the
+//    only runtime arrays are those that are the last member in a
+//    Block-decorated struct.  This allows us to feasibly/easily compute the
+//    length of the runtime array. See below.
+//
+//  - The memory locations accessed by OpLoad, OpStore, OpCopyMemory, and
+//    OpAtomic* are determined by their pointer parameter or parameters.
+//    Pointers are always (correctly) typed and so the address and number of
+//    consecutive locations are fully determined by the pointer.
+//
+//  - A pointer value orginates as one of few cases:
+//
+//    - OpVariable for an interface object or an array of them: image,
+//      buffer (UBO or SSBO), sampler, sampled-image, push-constant, input
+//      variable, output variable. The execution environment is responsible for
+//      allocating the correct amount of storage for these, and for ensuring
+//      each resource bound to such a variable is big enough to contain the
+//      SPIR-V pointee type of the variable.
+//
+//    - OpVariable for a non-interface object.  These are variables in
+//      Workgroup, Private, and Function storage classes.  The compiler ensures
+//      the underlying allocation is big enough to store the entire SPIR-V
+//      pointee type of the variable.
+//
+//    - An OpFunctionParameter. This always maps to a pointer parameter to an
+//      OpFunctionCall.
+//
+//      - In logical addressing mode, these are severely limited:
+//        "Any pointer operand to an OpFunctionCall must be:
+//          - a memory object declaration, or
+//          - a pointer to an element in an array that is a memory object
+//          declaration, where the element type is OpTypeSampler or OpTypeImage"
+//
+//      - This has an important simplifying consequence:
+//
+//        - When looking for a pointer to the structure containing a runtime
+//          array, you begin with a pointer to the runtime array and trace
+//          backward in the function.  You never have to trace back beyond
+//          your function call boundary.  So you can't take a partial access
+//          chain into an SSBO, then pass that pointer into a function.  So
+//          we don't resort to using fat pointers to compute array length.
+//          We can trace back to a pointer to the containing structure,
+//          and use that in an OpArrayLength instruction. (The structure type
+//          gives us the member index of the runtime array.)
+//
+//        - Otherwise, the pointer type fully encodes the range of valid
+//          addresses. In particular, the type of a pointer to an aggregate
+//          value fully encodes the range of indices when indexing into
+//          that aggregate.
+//
+//    - The pointer is the result of an access chain instruction.  We clamp
+//      indices contributing to address calculations.  As noted above, the
+//      valid ranges are either bound by the length of a runtime array, or
+//      by the type of the base pointer.  The length of a runtime array is
+//      the result of an OpArrayLength instruction acting on the pointer of
+//      the containing structure as noted above.
+//
+//    - TODO(dneto): OpImageTexelPointer:
+//      - Clamp coordinate to the image size returned by OpImageQuerySize
+//      - If multi-sampled, clamp the sample index to the count returned by
+//        OpImageQuerySamples.
+//      - If not multi-sampled, set the sample index to 0.
+//
+//  - Rely on the external validator to check that pointers are only
+//    used by the instructions as above.
+//
+//  - Handles OpTypeRuntimeArray
+//    Track pointer back to original resource (pointer to struct), so we can
+//    query the runtime array size.
+//
+
+#include "graphics_robust_access_pass.h"
+
+#include <algorithm>
+#include <cstring>
+#include <functional>
+#include <initializer_list>
+#include <utility>
+
+#include "constants.h"
+#include "def_use_manager.h"
+#include "function.h"
+#include "ir_context.h"
+#include "module.h"
+#include "pass.h"
+#include "source/diagnostic.h"
+#include "source/util/make_unique.h"
+#include "spirv-tools/libspirv.h"
+#include "spirv/unified1/GLSL.std.450.h"
+#include "spirv/unified1/spirv.h"
+#include "type_manager.h"
+#include "types.h"
+
+namespace spvtools {
+namespace opt {
+
+using opt::BasicBlock;
+using opt::Instruction;
+using opt::Operand;
+using spvtools::MakeUnique;
+
+GraphicsRobustAccessPass::GraphicsRobustAccessPass() : module_status_() {}
+
+Pass::Status GraphicsRobustAccessPass::Process() {
+  module_status_ = PerModuleState();
+
+  ProcessCurrentModule();
+
+  auto result = module_status_.failed
+                    ? Status::Failure
+                    : (module_status_.modified ? Status::SuccessWithChange
+                                               : Status::SuccessWithoutChange);
+
+  return result;
+}
+
+spvtools::DiagnosticStream GraphicsRobustAccessPass::Fail() {
+  module_status_.failed = true;
+  // We don't really have a position, and we'll ignore the result.
+  return std::move(
+      spvtools::DiagnosticStream({}, consumer(), "", SPV_ERROR_INVALID_BINARY)
+      << name() << ": ");
+}
+
+spv_result_t GraphicsRobustAccessPass::IsCompatibleModule() {
+  auto* feature_mgr = context()->get_feature_mgr();
+  if (!feature_mgr->HasCapability(SpvCapabilityShader))
+    return Fail() << "Can only process Shader modules";
+  if (feature_mgr->HasCapability(SpvCapabilityVariablePointers))
+    return Fail() << "Can't process modules with VariablePointers capability";
+  if (feature_mgr->HasCapability(SpvCapabilityVariablePointersStorageBuffer))
+    return Fail() << "Can't process modules with VariablePointersStorageBuffer "
+                     "capability";
+  if (feature_mgr->HasCapability(SpvCapabilityRuntimeDescriptorArrayEXT)) {
+    // These have a RuntimeArray outside of Block-decorated struct.  There
+    // is no way to compute the array length from within SPIR-V.
+    return Fail() << "Can't process modules with RuntimeDescriptorArrayEXT "
+                     "capability";
+  }
+
+  {
+    auto* inst = context()->module()->GetMemoryModel();
+    const auto addressing_model = inst->GetSingleWordOperand(0);
+    if (addressing_model != SpvAddressingModelLogical)
+      return Fail() << "Addressing model must be Logical.  Found "
+                    << inst->PrettyPrint();
+  }
+  return SPV_SUCCESS;
+}
+
+spv_result_t GraphicsRobustAccessPass::ProcessCurrentModule() {
+  auto err = IsCompatibleModule();
+  if (err != SPV_SUCCESS) return err;
+
+  ProcessFunction fn = [this](opt::Function* f) { return ProcessAFunction(f); };
+  module_status_.modified |= context()->ProcessReachableCallTree(fn);
+
+  // Need something here.  It's the price we pay for easier failure paths.
+  return SPV_SUCCESS;
+}
+
+bool GraphicsRobustAccessPass::ProcessAFunction(opt::Function* function) {
+  // Ensure that all pointers computed inside a function are within bounds.
+  // Find the access chains in this block before trying to modify them.
+  std::vector<Instruction*> access_chains;
+  std::vector<Instruction*> image_texel_pointers;
+  for (auto& block : *function) {
+    for (auto& inst : block) {
+      switch (inst.opcode()) {
+        case SpvOpAccessChain:
+        case SpvOpInBoundsAccessChain:
+          access_chains.push_back(&inst);
+          break;
+        case SpvOpImageTexelPointer:
+          image_texel_pointers.push_back(&inst);
+          break;
+        default:
+          break;
+      }
+    }
+  }
+  for (auto* inst : access_chains) {
+    ClampIndicesForAccessChain(inst);
+  }
+
+  for (auto* inst : image_texel_pointers) {
+    if (SPV_SUCCESS != ClampCoordinateForImageTexelPointer(inst)) break;
+  }
+  return module_status_.modified;
+}
+
+void GraphicsRobustAccessPass::ClampIndicesForAccessChain(
+    Instruction* access_chain) {
+  Instruction& inst = *access_chain;
+
+  auto* constant_mgr = context()->get_constant_mgr();
+  auto* def_use_mgr = context()->get_def_use_mgr();
+  auto* type_mgr = context()->get_type_mgr();
+
+  // Replaces one of the OpAccessChain index operands with a new value.
+  // Updates def-use analysis.
+  auto replace_index = [&inst, def_use_mgr](uint32_t operand_index,
+                                            Instruction* new_value) {
+    inst.SetOperand(operand_index, {new_value->result_id()});
+    def_use_mgr->AnalyzeInstUse(&inst);
+  };
+
+  // Replaces one of the OpAccesssChain index operands with a clamped value.
+  // Replace the operand at |operand_index| with the value computed from
+  // unsigned_clamp(%old_value, %min_value, %max_value).  It also analyzes
+  // the new instruction and records that them module is modified.
+  auto clamp_index = [&inst, this, &replace_index](
+                         uint32_t operand_index, Instruction* old_value,
+                         Instruction* min_value, Instruction* max_value) {
+    auto* clamp_inst = MakeClampInst(old_value, min_value, max_value, &inst);
+    replace_index(operand_index, clamp_inst);
+  };
+
+  // Ensures the specified index of access chain |inst| has a value that is
+  // at most |count| - 1.  If the index is already a constant value less than
+  // |count| then no change is made.
+  auto clamp_to_literal_count = [&inst, this, &constant_mgr, &type_mgr,
+                                 &replace_index, &clamp_index](
+                                    uint32_t operand_index, uint64_t count) {
+    Instruction* index_inst =
+        this->GetDef(inst.GetSingleWordOperand(operand_index));
+    const auto* index_type =
+        type_mgr->GetType(index_inst->type_id())->AsInteger();
+    assert(index_type);
+    if (count <= 1) {
+      // Replace the index with 0.
+      replace_index(operand_index, GetValueForType(0, index_type));
+      return;
+    }
+
+    const auto index_width = index_type->width();
+
+    // If the index is a constant then |index_constant| will not be a null
+    // pointer.  (If index is an |OpConstantNull| then it |index_constant| will
+    // not be a null pointer.)  Since access chain indices must be scalar
+    // integers, this can't be a spec constant.
+    if (auto* index_constant = constant_mgr->GetConstantFromInst(index_inst)) {
+      auto* int_index_constant = index_constant->AsIntConstant();
+      int64_t value = 0;
+      // OpAccessChain indices are treated as signed.  So get the signed
+      // constant value here.
+      if (index_width <= 32) {
+        value = int64_t(int_index_constant->GetS32BitValue());
+      } else if (index_width <= 64) {
+        value = int_index_constant->GetS64BitValue();
+      } else {
+        this->Fail() << "Can't handle indices wider than 64 bits, found "
+                        "constant index with "
+                     << index_type->width() << "bits";
+        return;
+      }
+      if (value < 0) {
+        replace_index(operand_index, GetValueForType(0, index_type));
+      } else if (uint64_t(value) < count) {
+        // Nothing to do.
+        return;
+      } else {
+        // Replace with count - 1.
+        assert(count > 0);  // Already took care of this case above.
+        replace_index(operand_index, GetValueForType(count - 1, index_type));
+      }
+    } else {
+      // Generate a clamp instruction.
+
+      // Compute the bit width of a viable type to hold (count-1).
+      const auto maxval = count - 1;
+      const auto* maxval_type = index_type;
+      // Look for a bit width, up to 64 bits wide, to fit maxval.
+      uint32_t maxval_width = index_width;
+      while ((maxval_width < 64) && (0 != (maxval >> maxval_width))) {
+        maxval_width *= 2;
+      }
+      // Widen the index value if necessary
+      if (maxval_width > index_width) {
+        // Find the wider type.  We only need this case if a constant (array)
+        // bound is too big.  This never requires us to *add* a capability
+        // declaration for Int64 because the existence of the array bound would
+        // already have required that declaration.
+        index_inst = WidenInteger(index_type->IsSigned(), maxval_width,
+                                  index_inst, &inst);
+        maxval_type = type_mgr->GetType(index_inst->type_id())->AsInteger();
+      }
+      // Finally, clamp the index.
+      clamp_index(operand_index, index_inst, GetValueForType(0, maxval_type),
+                  GetValueForType(maxval, maxval_type));
+    }
+  };
+
+  // Ensures the specified index of access chain |inst| has a value that is at
+  // most the value of |count_inst| minus 1, where |count_inst| is treated as an
+  // unsigned integer.
+  auto clamp_to_count = [&inst, this, &constant_mgr, &clamp_to_literal_count,
+                         &clamp_index, &type_mgr](uint32_t operand_index,
+                                                  Instruction* count_inst) {
+    Instruction* index_inst =
+        this->GetDef(inst.GetSingleWordOperand(operand_index));
+    const auto* index_type =
+        type_mgr->GetType(index_inst->type_id())->AsInteger();
+    const auto* count_type =
+        type_mgr->GetType(count_inst->type_id())->AsInteger();
+    assert(index_type);
+    if (const auto* count_constant =
+            constant_mgr->GetConstantFromInst(count_inst)) {
+      uint64_t value = 0;
+      const auto width = count_constant->type()->AsInteger()->width();
+      if (width <= 32) {
+        value = count_constant->AsIntConstant()->GetU32BitValue();
+      } else if (width <= 64) {
+        value = count_constant->AsIntConstant()->GetU64BitValue();
+      } else {
+        this->Fail() << "Can't handle indices wider than 64 bits, found "
+                        "constant index with "
+                     << index_type->width() << "bits";
+        return;
+      }
+      clamp_to_literal_count(operand_index, value);
+    } else {
+      // Widen them to the same width.
+      const auto index_width = index_type->width();
+      const auto count_width = count_type->width();
+      const auto target_width = std::max(index_width, count_width);
+      // UConvert requires the result type to have 0 signedness.  So enforce
+      // that here.
+      auto* wider_type = index_width < count_width ? count_type : index_type;
+      if (index_type->width() < target_width) {
+        // Access chain indices are treated as signed integers.
+        index_inst = WidenInteger(true, target_width, index_inst, &inst);
+      } else if (count_type->width() < target_width) {
+        // Assume type sizes are treated as unsigned.
+        count_inst = WidenInteger(false, target_width, count_inst, &inst);
+      }
+      // Compute count - 1.
+      // It doesn't matter if 1 is signed or unsigned.
+      auto* one = GetValueForType(1, wider_type);
+      auto* count_minus_1 = InsertInst(
+          &inst, SpvOpISub, type_mgr->GetId(wider_type), TakeNextId(),
+          {{SPV_OPERAND_TYPE_ID, {count_inst->result_id()}},
+           {SPV_OPERAND_TYPE_ID, {one->result_id()}}});
+      clamp_index(operand_index, index_inst, GetValueForType(0, wider_type),
+                  count_minus_1);
+    }
+  };
+
+  const Instruction* base_inst = GetDef(inst.GetSingleWordInOperand(0));
+  const Instruction* base_type = GetDef(base_inst->type_id());
+  Instruction* pointee_type = GetDef(base_type->GetSingleWordInOperand(1));
+
+  // Walk the indices from earliest to latest, replacing indices with a
+  // clamped value, and updating the pointee_type.  The order matters for
+  // the case when we have to compute the length of a runtime array.  In
+  // that the algorithm relies on the fact that that the earlier indices
+  // have already been clamped.
+  const uint32_t num_operands = inst.NumOperands();
+  for (uint32_t idx = 3; !module_status_.failed && idx < num_operands; ++idx) {
+    const uint32_t index_id = inst.GetSingleWordOperand(idx);
+    Instruction* index_inst = GetDef(index_id);
+
+    switch (pointee_type->opcode()) {
+      case SpvOpTypeMatrix:  // Use column count
+      case SpvOpTypeVector:  // Use component count
+      {
+        const uint32_t count = pointee_type->GetSingleWordOperand(2);
+        clamp_to_literal_count(idx, count);
+        pointee_type = GetDef(pointee_type->GetSingleWordOperand(1));
+      } break;
+
+      case SpvOpTypeArray: {
+        // The array length can be a spec constant, so go through the general
+        // case.
+        Instruction* array_len = GetDef(pointee_type->GetSingleWordOperand(2));
+        clamp_to_count(idx, array_len);
+        pointee_type = GetDef(pointee_type->GetSingleWordOperand(1));
+      } break;
+
+      case SpvOpTypeStruct: {
+        // SPIR-V requires the index to be an OpConstant.
+        // We need to know the index literal value so we can compute the next
+        // pointee type.
+        if (index_inst->opcode() != SpvOpConstant ||
+            !constant_mgr->GetConstantFromInst(index_inst)
+                 ->type()
+                 ->AsInteger()) {
+          Fail() << "Member index into struct is not a constant integer: "
+                 << index_inst->PrettyPrint(
+                        SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)
+                 << "\nin access chain: "
+                 << inst.PrettyPrint(SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+          return;
+        }
+        const auto num_members = pointee_type->NumInOperands();
+        const auto* index_constant =
+            constant_mgr->GetConstantFromInst(index_inst);
+        // Get the sign-extended value, since access index is always treated as
+        // signed.
+        const auto index_value = index_constant->GetSignExtendedValue();
+        if (index_value < 0 || index_value >= num_members) {
+          Fail() << "Member index " << index_value
+                 << " is out of bounds for struct type: "
+                 << pointee_type->PrettyPrint(
+                        SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)
+                 << "\nin access chain: "
+                 << inst.PrettyPrint(SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+          return;
+        }
+        pointee_type = GetDef(pointee_type->GetSingleWordInOperand(
+            static_cast<uint32_t>(index_value)));
+        // No need to clamp this index.  We just checked that it's valid.
+      } break;
+
+      case SpvOpTypeRuntimeArray: {
+        auto* array_len = MakeRuntimeArrayLengthInst(&inst, idx);
+        if (!array_len) {  // We've already signaled an error.
+          return;
+        }
+        clamp_to_count(idx, array_len);
+        pointee_type = GetDef(pointee_type->GetSingleWordOperand(1));
+      } break;
+
+      default:
+        Fail() << " Unhandled pointee type for access chain "
+               << pointee_type->PrettyPrint(
+                      SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+    }
+  }
+}
+
+uint32_t GraphicsRobustAccessPass::GetGlslInsts() {
+  if (module_status_.glsl_insts_id == 0) {
+    // This string serves double-duty as raw data for a string and for a vector
+    // of 32-bit words
+    const char glsl[] = "GLSL.std.450\0\0\0\0";
+    const size_t glsl_str_byte_len = 16;
+    // Use an existing import if we can.
+    for (auto& inst : context()->module()->ext_inst_imports()) {
+      const auto& name_words = inst.GetInOperand(0).words;
+      if (0 == std::strncmp(reinterpret_cast<const char*>(name_words.data()),
+                            glsl, glsl_str_byte_len)) {
+        module_status_.glsl_insts_id = inst.result_id();
+      }
+    }
+    if (module_status_.glsl_insts_id == 0) {
+      // Make a new import instruction.
+      module_status_.glsl_insts_id = TakeNextId();
+      std::vector<uint32_t> words(glsl_str_byte_len / sizeof(uint32_t));
+      std::memcpy(words.data(), glsl, glsl_str_byte_len);
+      auto import_inst = MakeUnique<Instruction>(
+          context(), SpvOpExtInstImport, 0, module_status_.glsl_insts_id,
+          std::initializer_list<Operand>{
+              Operand{SPV_OPERAND_TYPE_LITERAL_STRING, std::move(words)}});
+      Instruction* inst = import_inst.get();
+      context()->module()->AddExtInstImport(std::move(import_inst));
+      module_status_.modified = true;
+      context()->AnalyzeDefUse(inst);
+      // Reanalyze the feature list, since we added an extended instruction
+      // set improt.
+      context()->get_feature_mgr()->Analyze(context()->module());
+    }
+  }
+  return module_status_.glsl_insts_id;
+}
+
+opt::Instruction* opt::GraphicsRobustAccessPass::GetValueForType(
+    uint64_t value, const analysis::Integer* type) {
+  auto* mgr = context()->get_constant_mgr();
+  assert(type->width() <= 64);
+  std::vector<uint32_t> words;
+  words.push_back(uint32_t(value));
+  if (type->width() > 32) {
+    words.push_back(uint32_t(value >> 32u));
+  }
+  const auto* constant = mgr->GetConstant(type, words);
+  return mgr->GetDefiningInstruction(
+      constant, context()->get_type_mgr()->GetTypeInstruction(type));
+}
+
+opt::Instruction* opt::GraphicsRobustAccessPass::WidenInteger(
+    bool sign_extend, uint32_t bit_width, Instruction* value,
+    Instruction* before_inst) {
+  analysis::Integer unsigned_type_for_query(bit_width, false);
+  auto* type_mgr = context()->get_type_mgr();
+  auto* unsigned_type = type_mgr->GetRegisteredType(&unsigned_type_for_query);
+  auto type_id = context()->get_type_mgr()->GetId(unsigned_type);
+  auto conversion_id = TakeNextId();
+  auto* conversion = InsertInst(
+      before_inst, (sign_extend ? SpvOpSConvert : SpvOpUConvert), type_id,
+      conversion_id, {{SPV_OPERAND_TYPE_ID, {value->result_id()}}});
+  return conversion;
+}
+
+Instruction* GraphicsRobustAccessPass::MakeClampInst(Instruction* x,
+                                                     Instruction* min,
+                                                     Instruction* max,
+                                                     Instruction* where) {
+  // Get IDs of instructions we'll be referencing. Evaluate them before calling
+  // the function so we force a deterministic ordering in case both of them need
+  // to take a new ID.
+  const uint32_t glsl_insts_id = GetGlslInsts();
+  uint32_t clamp_id = TakeNextId();
+  assert(x->type_id() == min->type_id());
+  assert(x->type_id() == max->type_id());
+  auto* clamp_inst = InsertInst(
+      where, SpvOpExtInst, x->type_id(), clamp_id,
+      {
+          {SPV_OPERAND_TYPE_ID, {glsl_insts_id}},
+          {SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {GLSLstd450UClamp}},
+          {SPV_OPERAND_TYPE_ID, {x->result_id()}},
+          {SPV_OPERAND_TYPE_ID, {min->result_id()}},
+          {SPV_OPERAND_TYPE_ID, {max->result_id()}},
+      });
+  return clamp_inst;
+}
+
+Instruction* GraphicsRobustAccessPass::MakeRuntimeArrayLengthInst(
+    Instruction* access_chain, uint32_t operand_index) {
+  // The Index parameter to the access chain at |operand_index| is indexing
+  // *into* the runtime-array.  To get the number of elements in the runtime
+  // array we need a pointer to the Block-decorated struct that contains the
+  // runtime array. So conceptually we have to go 2 steps backward in the
+  // access chain.  The two steps backward might forces us to traverse backward
+  // across multiple dominating instructions.
+  auto* type_mgr = context()->get_type_mgr();
+
+  // How many access chain indices do we have to unwind to find the pointer
+  // to the struct containing the runtime array?
+  uint32_t steps_remaining = 2;
+  // Find or create an instruction computing the pointer to the structure
+  // containing the runtime array.
+  // Walk backward through pointer address calculations until we either get
+  // to exactly the right base pointer, or to an access chain instruction
+  // that we can replicate but truncate to compute the address of the right
+  // struct.
+  Instruction* current_access_chain = access_chain;
+  Instruction* pointer_to_containing_struct = nullptr;
+  while (steps_remaining > 0) {
+    switch (current_access_chain->opcode()) {
+      case SpvOpCopyObject:
+        // Whoops. Walk right through this one.
+        current_access_chain =
+            GetDef(current_access_chain->GetSingleWordInOperand(0));
+        break;
+      case SpvOpAccessChain:
+      case SpvOpInBoundsAccessChain: {
+        const int first_index_operand = 3;
+        // How many indices in this access chain contribute to getting us
+        // to an element in the runtime array?
+        const auto num_contributing_indices =
+            current_access_chain == access_chain
+                ? operand_index - (first_index_operand - 1)
+                : current_access_chain->NumInOperands() - 1 /* skip the base */;
+        Instruction* base =
+            GetDef(current_access_chain->GetSingleWordInOperand(0));
+        if (num_contributing_indices == steps_remaining) {
+          // The base pointer points to the structure.
+          pointer_to_containing_struct = base;
+          steps_remaining = 0;
+          break;
+        } else if (num_contributing_indices < steps_remaining) {
+          // Peel off the index and keep going backward.
+          steps_remaining -= num_contributing_indices;
+          current_access_chain = base;
+        } else {
+          // This access chain has more indices than needed.  Generate a new
+          // access chain instruction, but truncating the list of indices.
+          const int base_operand = 2;
+          // We'll use the base pointer and the indices up to but not including
+          // the one indexing into the runtime array.
+          Instruction::OperandList ops;
+          // Use the base pointer
+          ops.push_back(current_access_chain->GetOperand(base_operand));
+          const uint32_t num_indices_to_keep =
+              num_contributing_indices - steps_remaining - 1;
+          for (uint32_t i = 0; i <= num_indices_to_keep; i++) {
+            ops.push_back(
+                current_access_chain->GetOperand(first_index_operand + i));
+          }
+          // Compute the type of the result of the new access chain.  Start at
+          // the base and walk the indices in a forward direction.
+          auto* constant_mgr = context()->get_constant_mgr();
+          std::vector<uint32_t> indices_for_type;
+          for (uint32_t i = 0; i < ops.size() - 1; i++) {
+            uint32_t index_for_type_calculation = 0;
+            Instruction* index =
+                GetDef(current_access_chain->GetSingleWordOperand(
+                    first_index_operand + i));
+            if (auto* index_constant =
+                    constant_mgr->GetConstantFromInst(index)) {
+              // We only need 32 bits. For the type calculation, it's sufficient
+              // to take the zero-extended value. It only matters for the struct
+              // case, and struct member indices are unsigned.
+              index_for_type_calculation =
+                  uint32_t(index_constant->GetZeroExtendedValue());
+            } else {
+              // Indexing into a variably-sized thing like an array.  Use 0.
+              index_for_type_calculation = 0;
+            }
+            indices_for_type.push_back(index_for_type_calculation);
+          }
+          auto* base_ptr_type = type_mgr->GetType(base->type_id())->AsPointer();
+          auto* base_pointee_type = base_ptr_type->pointee_type();
+          auto* new_access_chain_result_pointee_type =
+              type_mgr->GetMemberType(base_pointee_type, indices_for_type);
+          const uint32_t new_access_chain_type_id = type_mgr->FindPointerToType(
+              type_mgr->GetId(new_access_chain_result_pointee_type),
+              base_ptr_type->storage_class());
+
+          // Create the instruction and insert it.
+          const auto new_access_chain_id = TakeNextId();
+          auto* new_access_chain =
+              InsertInst(current_access_chain, current_access_chain->opcode(),
+                         new_access_chain_type_id, new_access_chain_id, ops);
+          pointer_to_containing_struct = new_access_chain;
+          steps_remaining = 0;
+          break;
+        }
+      } break;
+      default:
+        Fail() << "Unhandled access chain in logical addressing mode passes "
+                  "through "
+               << current_access_chain->PrettyPrint(
+                      SPV_BINARY_TO_TEXT_OPTION_SHOW_BYTE_OFFSET |
+                      SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+        return nullptr;
+    }
+  }
+  assert(pointer_to_containing_struct);
+  auto* pointee_type =
+      type_mgr->GetType(pointer_to_containing_struct->type_id())
+          ->AsPointer()
+          ->pointee_type();
+
+  auto* struct_type = pointee_type->AsStruct();
+  const uint32_t member_index_of_runtime_array =
+      uint32_t(struct_type->element_types().size() - 1);
+  // Create the length-of-array instruction before the original access chain,
+  // but after the generation of the pointer to the struct.
+  const auto array_len_id = TakeNextId();
+  analysis::Integer uint_type_for_query(32, false);
+  auto* uint_type = type_mgr->GetRegisteredType(&uint_type_for_query);
+  auto* array_len = InsertInst(
+      access_chain, SpvOpArrayLength, type_mgr->GetId(uint_type), array_len_id,
+      {{SPV_OPERAND_TYPE_ID, {pointer_to_containing_struct->result_id()}},
+       {SPV_OPERAND_TYPE_LITERAL_INTEGER, {member_index_of_runtime_array}}});
+  return array_len;
+}
+
+spv_result_t GraphicsRobustAccessPass::ClampCoordinateForImageTexelPointer(
+    opt::Instruction* image_texel_pointer) {
+  // TODO(dneto): Write tests for this code.
+  return SPV_SUCCESS;
+
+  // Example:
+  //   %texel_ptr = OpImageTexelPointer %texel_ptr_type %image_ptr %coord
+  //   %sample
+  //
+  // We want to clamp %coord components between vector-0 and the result
+  // of OpImageQuerySize acting on the underlying image.  So insert:
+  //     %image = OpLoad %image_type %image_ptr
+  //     %query_size = OpImageQuerySize %query_size_type %image
+  //
+  // For a multi-sampled image, %sample is the sample index, and we need
+  // to clamp it between zero and the number of samples in the image.
+  //     %sample_count = OpImageQuerySamples %uint %image
+  //     %max_sample_index = OpISub %uint %sample_count %uint_1
+  // For non-multi-sampled images, the sample index must be constant zero.
+
+  auto* def_use_mgr = context()->get_def_use_mgr();
+  auto* type_mgr = context()->get_type_mgr();
+  auto* constant_mgr = context()->get_constant_mgr();
+
+  auto* image_ptr = GetDef(image_texel_pointer->GetSingleWordInOperand(0));
+  auto* image_ptr_type = GetDef(image_ptr->type_id());
+  auto image_type_id = image_ptr_type->GetSingleWordInOperand(1);
+  auto* image_type = GetDef(image_type_id);
+  auto* coord = GetDef(image_texel_pointer->GetSingleWordInOperand(1));
+  auto* samples = GetDef(image_texel_pointer->GetSingleWordInOperand(2));
+
+  // We will modify the module, at least by adding image query instructions.
+  module_status_.modified = true;
+
+  // Declare the ImageQuery capability if the module doesn't already have it.
+  auto* feature_mgr = context()->get_feature_mgr();
+  if (!feature_mgr->HasCapability(SpvCapabilityImageQuery)) {
+    auto cap = MakeUnique<Instruction>(
+        context(), SpvOpCapability, 0, 0,
+        std::initializer_list<Operand>{
+            {SPV_OPERAND_TYPE_CAPABILITY, {SpvCapabilityImageQuery}}});
+    def_use_mgr->AnalyzeInstDefUse(cap.get());
+    context()->AddCapability(std::move(cap));
+    feature_mgr->Analyze(context()->module());
+  }
+
+  // OpImageTexelPointer is used to translate a coordinate and sample index
+  // into an address for use with an atomic operation.  That is, it may only
+  // used with what Vulkan calls a "storage image"
+  // (OpTypeImage parameter Sampled=2).
+  // Note: A storage image never has a level-of-detail associated with it.
+
+  // Constraints on the sample id:
+  //  - Only 2D images can be multi-sampled: OpTypeImage parameter MS=1
+  //    only if Dim=2D.
+  //  - Non-multi-sampled images (OpTypeImage parameter MS=0) must use
+  //    sample ID to a constant 0.
+
+  // The coordinate is treated as unsigned, and should be clamped against the
+  // image "size", returned by OpImageQuerySize. (Note: OpImageQuerySizeLod
+  // is only usable with a sampled image, i.e. its image type has Sampled=1).
+
+  // Determine the result type for the OpImageQuerySize.
+  // For non-arrayed images:
+  //   non-Cube:
+  //     - Always the same as the coordinate type
+  //   Cube:
+  //     - Use all but the last component of the coordinate (which is the face
+  //       index from 0 to 5).
+  // For arrayed images (in Vulkan the Dim is 1D, 2D, or Cube):
+  //   non-Cube:
+  //     - A vector with the components in the coordinate, and one more for
+  //       the layer index.
+  //   Cube:
+  //     - The same as the coordinate type: 3-element integer vector.
+  //     - The third component from the size query is the layer count.
+  //     - The third component in the texel pointer calculation is
+  //       6 * layer + face, where 0 <= face < 6.
+  //   Cube: Use all but the last component of the coordinate (which is the face
+  //   index from 0 to 5).
+  const auto dim = SpvDim(image_type->GetSingleWordInOperand(1));
+  const bool arrayed = image_type->GetSingleWordInOperand(3) == 1;
+  const bool multisampled = image_type->GetSingleWordInOperand(4) != 0;
+  const auto query_num_components = [dim, arrayed, this]() -> int {
+    const int arrayness_bonus = arrayed ? 1 : 0;
+    int num_coords = 0;
+    switch (dim) {
+      case SpvDimBuffer:
+      case SpvDim1D:
+        num_coords = 1;
+        break;
+      case SpvDimCube:
+        // For cube, we need bounds for x, y, but not face.
+      case SpvDimRect:
+      case SpvDim2D:
+        num_coords = 2;
+        break;
+      case SpvDim3D:
+        num_coords = 3;
+        break;
+      case SpvDimSubpassData:
+      case SpvDimMax:
+        return Fail() << "Invalid image dimension for OpImageTexelPointer: "
+                      << int(dim);
+        break;
+    }
+    return num_coords + arrayness_bonus;
+  }();
+  const auto* coord_component_type = [type_mgr, coord]() {
+    const analysis::Type* coord_type = type_mgr->GetType(coord->type_id());
+    if (auto* vector_type = coord_type->AsVector()) {
+      return vector_type->element_type()->AsInteger();
+    }
+    return coord_type->AsInteger();
+  }();
+  // For now, only handle 32-bit case for coordinates.
+  if (!coord_component_type) {
+    return Fail() << " Coordinates for OpImageTexelPointer are not integral: "
+                  << image_texel_pointer->PrettyPrint(
+                         SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+  }
+  if (coord_component_type->width() != 32) {
+    return Fail() << " Expected OpImageTexelPointer coordinate components to "
+                     "be 32-bits wide. They are "
+                  << coord_component_type->width() << " bits. "
+                  << image_texel_pointer->PrettyPrint(
+                         SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+  }
+  const auto* query_size_type =
+      [type_mgr, coord_component_type,
+       query_num_components]() -> const analysis::Type* {
+    if (query_num_components == 1) return coord_component_type;
+    analysis::Vector proposed(coord_component_type, query_num_components);
+    return type_mgr->GetRegisteredType(&proposed);
+  }();
+
+  const uint32_t image_id = TakeNextId();
+  auto* image =
+      InsertInst(image_texel_pointer, SpvOpLoad, image_type_id, image_id,
+                 {{SPV_OPERAND_TYPE_ID, {image_ptr->result_id()}}});
+
+  const uint32_t query_size_id = TakeNextId();
+  auto* query_size =
+      InsertInst(image_texel_pointer, SpvOpImageQuerySize,
+                 type_mgr->GetTypeInstruction(query_size_type), query_size_id,
+                 {{SPV_OPERAND_TYPE_ID, {image->result_id()}}});
+
+  auto* component_1 = constant_mgr->GetConstant(coord_component_type, {1});
+  const uint32_t component_1_id =
+      constant_mgr->GetDefiningInstruction(component_1)->result_id();
+  auto* component_0 = constant_mgr->GetConstant(coord_component_type, {0});
+  const uint32_t component_0_id =
+      constant_mgr->GetDefiningInstruction(component_0)->result_id();
+
+  // If the image is a cube array, then the last component of the queried
+  // size is the layer count.  In the query, we have to accomodate folding
+  // in the face index ranging from 0 through 5. The inclusive upper bound
+  // on the third coordinate therefore is multiplied by 6.
+  auto* query_size_including_faces = query_size;
+  if (arrayed && (dim == SpvDimCube)) {
+    // Multiply the last coordinate by 6.
+    auto* component_6 = constant_mgr->GetConstant(coord_component_type, {6});
+    const uint32_t component_6_id =
+        constant_mgr->GetDefiningInstruction(component_6)->result_id();
+    assert(query_num_components == 3);
+    auto* multiplicand = constant_mgr->GetConstant(
+        query_size_type, {component_1_id, component_1_id, component_6_id});
+    auto* multiplicand_inst =
+        constant_mgr->GetDefiningInstruction(multiplicand);
+    const auto query_size_including_faces_id = TakeNextId();
+    query_size_including_faces = InsertInst(
+        image_texel_pointer, SpvOpIMul,
+        type_mgr->GetTypeInstruction(query_size_type),
+        query_size_including_faces_id,
+        {{SPV_OPERAND_TYPE_ID, {query_size_including_faces->result_id()}},
+         {SPV_OPERAND_TYPE_ID, {multiplicand_inst->result_id()}}});
+  }
+
+  // Make a coordinate-type with all 1 components.
+  auto* coordinate_1 =
+      query_num_components == 1
+          ? component_1
+          : constant_mgr->GetConstant(
+                query_size_type,
+                std::vector<uint32_t>(query_num_components, component_1_id));
+  // Make a coordinate-type with all 1 components.
+  auto* coordinate_0 =
+      query_num_components == 0
+          ? component_0
+          : constant_mgr->GetConstant(
+                query_size_type,
+                std::vector<uint32_t>(query_num_components, component_0_id));
+
+  const uint32_t query_max_including_faces_id = TakeNextId();
+  auto* query_max_including_faces = InsertInst(
+      image_texel_pointer, SpvOpISub,
+      type_mgr->GetTypeInstruction(query_size_type),
+      query_max_including_faces_id,
+      {{SPV_OPERAND_TYPE_ID, {query_size_including_faces->result_id()}},
+       {SPV_OPERAND_TYPE_ID,
+        {constant_mgr->GetDefiningInstruction(coordinate_1)->result_id()}}});
+
+  // Clamp the coordinate
+  auto* clamp_coord =
+      MakeClampInst(coord, constant_mgr->GetDefiningInstruction(coordinate_0),
+                    query_max_including_faces, image_texel_pointer);
+  image_texel_pointer->SetInOperand(1, {clamp_coord->result_id()});
+
+  // Clamp the sample index
+  if (multisampled) {
+    // Get the sample count via OpImageQuerySamples
+    const auto query_samples_id = TakeNextId();
+    auto* query_samples = InsertInst(
+        image_texel_pointer, SpvOpImageQuerySamples,
+        constant_mgr->GetDefiningInstruction(component_0)->type_id(),
+        query_samples_id, {{SPV_OPERAND_TYPE_ID, {image->result_id()}}});
+
+    const auto max_samples_id = TakeNextId();
+    auto* max_samples = InsertInst(image_texel_pointer, SpvOpImageQuerySamples,
+                                   query_samples->type_id(), max_samples_id,
+                                   {{SPV_OPERAND_TYPE_ID, {query_samples_id}},
+                                    {SPV_OPERAND_TYPE_ID, {component_1_id}}});
+
+    auto* clamp_samples = MakeClampInst(
+        samples, constant_mgr->GetDefiningInstruction(coordinate_0),
+        max_samples, image_texel_pointer);
+    image_texel_pointer->SetInOperand(2, {clamp_samples->result_id()});
+
+  } else {
+    // Just replace it with 0.  Don't even check what was there before.
+    image_texel_pointer->SetInOperand(2, {component_0_id});
+  }
+
+  def_use_mgr->AnalyzeInstUse(image_texel_pointer);
+
+  return SPV_SUCCESS;
+}
+
+opt::Instruction* GraphicsRobustAccessPass::InsertInst(
+    opt::Instruction* where_inst, SpvOp opcode, uint32_t type_id,
+    uint32_t result_id, const Instruction::OperandList& operands) {
+  module_status_.modified = true;
+  auto* result = where_inst->InsertBefore(
+      MakeUnique<Instruction>(context(), opcode, type_id, result_id, operands));
+  context()->get_def_use_mgr()->AnalyzeInstDefUse(result);
+  auto* basic_block = context()->get_instr_block(where_inst);
+  context()->set_instr_block(result, basic_block);
+  return result;
+}
+
+}  // namespace opt
+}  // namespace spvtools
diff --git a/source/opt/graphics_robust_access_pass.h b/source/opt/graphics_robust_access_pass.h
new file mode 100644
index 0000000..f9d17ca
--- /dev/null
+++ b/source/opt/graphics_robust_access_pass.h
@@ -0,0 +1,146 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_OPT_GRAPHICS_ROBUST_ACCESS_PASS_H_
+#define SOURCE_OPT_GRAPHICS_ROBUST_ACCESS_PASS_H_
+
+#include <map>
+#include <unordered_map>
+
+#include "source/diagnostic.h"
+
+#include "constants.h"
+#include "def_use_manager.h"
+#include "instruction.h"
+#include "module.h"
+#include "pass.h"
+
+namespace spvtools {
+namespace opt {
+
+// See optimizer.hpp for documentation.
+class GraphicsRobustAccessPass : public Pass {
+ public:
+  GraphicsRobustAccessPass();
+  const char* name() const override { return "graphics-robust-access"; }
+  Status Process() override;
+
+  IRContext::Analysis GetPreservedAnalyses() override {
+    return IRContext::kAnalysisDefUse |
+           IRContext::kAnalysisInstrToBlockMapping |
+           IRContext::kAnalysisConstants | IRContext::kAnalysisTypes |
+           IRContext::kAnalysisIdToFuncMapping;
+  };
+
+ private:
+  // Records failure for the current module, and returns a stream
+  // that can be used to provide user error information to the message
+  // consumer.
+  spvtools::DiagnosticStream Fail();
+
+  // Returns SPV_SUCCESS if this pass can correctly process the module.
+  // Otherwise logs a message and returns a failure code.
+  spv_result_t IsCompatibleModule();
+
+  // Transform the current module, if possible. Failure and modification
+  // status is recorded in the |_| member. On failure, error information is
+  // posted to the message consumer.  The return value has no significance.
+  spv_result_t ProcessCurrentModule();
+
+  // Process the given function.  Updates the state value |_|.  Returns true
+  // if the module was modified.
+  bool ProcessAFunction(opt::Function*);
+
+  // Clamps indices in the OpAccessChain or OpInBoundsAccessChain instruction
+  // |access_chain|. Inserts instructions before the given instruction.  Updates
+  // analyses and records that the module is modified.
+  void ClampIndicesForAccessChain(Instruction* access_chain);
+
+  // Returns the id of the instruction importing the "GLSL.std.450" extended
+  // instruction set. If it does not yet exist, the import instruction is
+  // created and inserted into the module, and updates |_.modified| and
+  // |_.glsl_insts_id|.
+  uint32_t GetGlslInsts();
+
+  // Returns an instruction which is constant with the given value of the given
+  // type. Ignores any value bits beyond the width of the type.
+  Instruction* GetValueForType(uint64_t value, const analysis::Integer* type);
+
+  // Returns the unsigned value for the given constant.  Assumes it's at most
+  // 64 bits wide.
+  uint64_t GetUnsignedValueForConstant(const analysis::Constant* c);
+
+  // Converts an integer value to an unsigned wider integer type, using either
+  // sign extension or zero extension.  The new instruction is inserted
+  // immediately before |before_inst|, and is analyzed for definitions and uses.
+  // Returns the newly inserted instruction.  Assumes the |value| is an integer
+  // scalar of a narrower type than |bitwidth| bits.
+  Instruction* WidenInteger(bool sign_extend, uint32_t bitwidth,
+                            Instruction* value, Instruction* before_inst);
+
+  // Returns a new instruction that invokes the UClamp GLSL.std.450 extended
+  // instruction with the three given operands.  That is, the result of the
+  // instruction is:
+  //  - |min| if |x| is unsigned-less than |min|
+  //  - |max| if |x| is unsigned-more than |max|
+  //  - |x| otherwise.
+  // We assume that |min| is unsigned-less-or-equal to |max|, and that the
+  // operands all have the same scalar integer type.  The instruction is
+  // inserted before |where|.
+  opt::Instruction* MakeClampInst(Instruction* x, Instruction* min,
+                                  Instruction* max, Instruction* where);
+
+  // Returns a new instruction which evaluates to the length the runtime array
+  // referenced by the access chain at the specfied index.  The instruction is
+  // inserted before the access chain instruction.  Returns a null pointer in
+  // some cases if assumptions are violated (rather than asserting out).
+  opt::Instruction* MakeRuntimeArrayLengthInst(Instruction* access_chain,
+                                               uint32_t operand_index);
+
+  // Clamps the coordinate for an OpImageTexelPointer so it stays within
+  // the bounds of the size of the image.  Updates analyses and records that
+  // the module is modified.  Returns a status code to indicate success
+  // or failure.  If assumptions are not met, returns an error status code
+  // and emits a diagnostic.
+  spv_result_t ClampCoordinateForImageTexelPointer(opt::Instruction* itp);
+
+  // Gets the instruction that defines the given id.
+  opt::Instruction* GetDef(uint32_t id) {
+    return context()->get_def_use_mgr()->GetDef(id);
+  }
+
+  // Returns a new instruction inserted before |where_inst|, and created from
+  // the remaining arguments. Registers the definitions and uses of the new
+  // instruction and also records its block.
+  opt::Instruction* InsertInst(opt::Instruction* where_inst, SpvOp opcode,
+                               uint32_t type_id, uint32_t result_id,
+                               const Instruction::OperandList& operands);
+
+  // State required for the current module.
+  struct PerModuleState {
+    // This pass modified the module.
+    bool modified = false;
+    // True if there is an error processing the current module, e.g. if
+    // preconditions are not met.
+    bool failed = false;
+    // The id of the GLSL.std.450 extended instruction set.  Zero if it does
+    // not exist.
+    uint32_t glsl_insts_id = 0;
+  } module_status_;
+};
+
+}  // namespace opt
+}  // namespace spvtools
+
+#endif  // SOURCE_OPT_GRAPHICS_ROBUST_ACCESS_PASS_H_
diff --git a/source/opt/instruction.h b/source/opt/instruction.h
index d507d6c..d1c4ce1 100644
--- a/source/opt/instruction.h
+++ b/source/opt/instruction.h
@@ -399,7 +399,7 @@
   inline bool operator<(const Instruction&) const;
 
   // Takes ownership of the instruction owned by |i| and inserts it immediately
-  // before |this|. Returns the insterted instruction.
+  // before |this|. Returns the inserted instruction.
   Instruction* InsertBefore(std::unique_ptr<Instruction>&& i);
   // Takes ownership of the instructions in |list| and inserts them in order
   // immediately before |this|.  Returns the first inserted instruction.
diff --git a/source/opt/optimizer.cpp b/source/opt/optimizer.cpp
index 5c1e6ca..2dd1708 100644
--- a/source/opt/optimizer.cpp
+++ b/source/opt/optimizer.cpp
@@ -22,6 +22,7 @@
 
 #include <source/spirv_optimizer_options.h>
 #include "source/opt/build_module.h"
+#include "source/opt/graphics_robust_access_pass.h"
 #include "source/opt/log.h"
 #include "source/opt/pass_manager.h"
 #include "source/opt/passes.h"
@@ -472,6 +473,8 @@
     RegisterPass(CreateLegalizeVectorShufflePass());
   } else if (pass_name == "decompose-initialized-variables") {
     RegisterPass(CreateDecomposeInitializedVariablesPass());
+  } else if (pass_name == "graphics-robust-access") {
+    RegisterPass(CreateGraphicsRobustAccessPass());
   } else {
     Errorf(consumer(), nullptr, {},
            "Unknown flag '--%s'. Use --help for a list of valid flags",
@@ -878,4 +881,9 @@
       MakeUnique<opt::SplitInvalidUnreachablePass>());
 }
 
+Optimizer::PassToken CreateGraphicsRobustAccessPass() {
+  return MakeUnique<Optimizer::PassToken::Impl>(
+      MakeUnique<opt::GraphicsRobustAccessPass>());
+}
+
 }  // namespace spvtools
diff --git a/source/opt/passes.h b/source/opt/passes.h
index 0a348e4..5eddc22 100644
--- a/source/opt/passes.h
+++ b/source/opt/passes.h
@@ -37,6 +37,7 @@
 #include "source/opt/fold_spec_constant_op_and_composite_pass.h"
 #include "source/opt/freeze_spec_constant_value_pass.h"
 #include "source/opt/generate_webgpu_initializers_pass.h"
+#include "source/opt/graphics_robust_access_pass.h"
 #include "source/opt/if_conversion.h"
 #include "source/opt/inline_exhaustive_pass.h"
 #include "source/opt/inline_opaque_pass.h"
diff --git a/test/opt/CMakeLists.txt b/test/opt/CMakeLists.txt
index 246c116..6131c9b 100644
--- a/test/opt/CMakeLists.txt
+++ b/test/opt/CMakeLists.txt
@@ -25,6 +25,7 @@
        code_sink_test.cpp
        combine_access_chains_test.cpp
        compact_ids_test.cpp
+       constants_test.cpp
        constant_manager_test.cpp
        copy_prop_array_test.cpp
        dead_branch_elim_test.cpp
@@ -44,6 +45,7 @@
        freeze_spec_const_test.cpp
        function_test.cpp
        generate_webgpu_initializers_test.cpp
+       graphics_robust_access_test.cpp
        if_conversion_test.cpp
        inline_opaque_test.cpp
        inline_test.cpp
diff --git a/test/opt/constants_test.cpp b/test/opt/constants_test.cpp
new file mode 100644
index 0000000..55c92a5
--- /dev/null
+++ b/test/opt/constants_test.cpp
@@ -0,0 +1,167 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/opt/constants.h"
+
+#include <gtest/gtest-param-test.h>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "source/opt/types.h"
+
+namespace spvtools {
+namespace opt {
+namespace analysis {
+namespace {
+
+using ConstantTest = ::testing::Test;
+using ::testing::ValuesIn;
+
+template <typename T>
+struct GetExtendedValueCase {
+  bool is_signed;
+  int width;
+  std::vector<uint32_t> words;
+  T expected_value;
+};
+
+using GetSignExtendedValueCase = GetExtendedValueCase<int64_t>;
+using GetZeroExtendedValueCase = GetExtendedValueCase<uint64_t>;
+
+using GetSignExtendedValueTest =
+    ::testing::TestWithParam<GetSignExtendedValueCase>;
+using GetZeroExtendedValueTest =
+    ::testing::TestWithParam<GetZeroExtendedValueCase>;
+
+TEST_P(GetSignExtendedValueTest, Case) {
+  Integer type(GetParam().width, GetParam().is_signed);
+  IntConstant value(&type, GetParam().words);
+
+  EXPECT_EQ(GetParam().expected_value, value.GetSignExtendedValue());
+}
+
+TEST_P(GetZeroExtendedValueTest, Case) {
+  Integer type(GetParam().width, GetParam().is_signed);
+  IntConstant value(&type, GetParam().words);
+
+  EXPECT_EQ(GetParam().expected_value, value.GetZeroExtendedValue());
+}
+
+const uint32_t k32ones = ~uint32_t(0);
+const uint64_t k64ones = ~uint64_t(0);
+const int64_t kSBillion = 1000 * 1000 * 1000;
+const uint64_t kUBillion = 1000 * 1000 * 1000;
+
+INSTANTIATE_TEST_SUITE_P(AtMost32Bits, GetSignExtendedValueTest,
+                         ValuesIn(std::vector<GetSignExtendedValueCase>{
+                             // 4 bits
+                             {false, 4, {0}, 0},
+                             {false, 4, {7}, 7},
+                             {false, 4, {15}, 15},
+                             {true, 4, {0}, 0},
+                             {true, 4, {7}, 7},
+                             {true, 4, {0xfffffff8}, -8},
+                             {true, 4, {k32ones}, -1},
+                             // 16 bits
+                             {false, 16, {0}, 0},
+                             {false, 16, {32767}, 32767},
+                             {false, 16, {32768}, 32768},
+                             {false, 16, {65000}, 65000},
+                             {true, 16, {0}, 0},
+                             {true, 16, {32767}, 32767},
+                             {true, 16, {0xfffffff8}, -8},
+                             {true, 16, {k32ones}, -1},
+                             // 32 bits
+                             {false, 32, {0}, 0},
+                             {false, 32, {1000000}, 1000000},
+                             {true, 32, {0xfffffff8}, -8},
+                             {true, 32, {k32ones}, -1},
+                         }));
+
+INSTANTIATE_TEST_SUITE_P(AtMost64Bits, GetSignExtendedValueTest,
+                         ValuesIn(std::vector<GetSignExtendedValueCase>{
+                             // 48 bits
+                             {false, 48, {0, 0}, 0},
+                             {false, 48, {5, 0}, 5},
+                             {false, 48, {0xfffffff8, k32ones}, -8},
+                             {false, 48, {k32ones, k32ones}, -1},
+                             {false, 48, {0xdcd65000, 1}, 8 * kSBillion},
+                             {true, 48, {0xfffffff8, k32ones}, -8},
+                             {true, 48, {k32ones, k32ones}, -1},
+                             {true, 48, {0xdcd65000, 1}, 8 * kSBillion},
+
+                             // 64 bits
+                             {false, 64, {12, 0}, 12},
+                             {false, 64, {0xdcd65000, 1}, 8 * kSBillion},
+                             {false, 48, {0xfffffff8, k32ones}, -8},
+                             {false, 64, {k32ones, k32ones}, -1},
+                             {true, 64, {12, 0}, 12},
+                             {true, 64, {0xdcd65000, 1}, 8 * kSBillion},
+                             {true, 48, {0xfffffff8, k32ones}, -8},
+                             {true, 64, {k32ones, k32ones}, -1},
+                         }));
+
+INSTANTIATE_TEST_SUITE_P(AtMost32Bits, GetZeroExtendedValueTest,
+                         ValuesIn(std::vector<GetZeroExtendedValueCase>{
+                             // 4 bits
+                             {false, 4, {0}, 0},
+                             {false, 4, {7}, 7},
+                             {false, 4, {15}, 15},
+                             {true, 4, {0}, 0},
+                             {true, 4, {7}, 7},
+                             {true, 4, {0xfffffff8}, 0xfffffff8},
+                             {true, 4, {k32ones}, k32ones},
+                             // 16 bits
+                             {false, 16, {0}, 0},
+                             {false, 16, {32767}, 32767},
+                             {false, 16, {32768}, 32768},
+                             {false, 16, {65000}, 65000},
+                             {true, 16, {0}, 0},
+                             {true, 16, {32767}, 32767},
+                             {true, 16, {0xfffffff8}, 0xfffffff8},
+                             {true, 16, {k32ones}, k32ones},
+                             // 32 bits
+                             {false, 32, {0}, 0},
+                             {false, 32, {1000000}, 1000000},
+                             {true, 32, {0xfffffff8}, 0xfffffff8},
+                             {true, 32, {k32ones}, k32ones},
+                         }));
+
+INSTANTIATE_TEST_SUITE_P(AtMost64Bits, GetZeroExtendedValueTest,
+                         ValuesIn(std::vector<GetZeroExtendedValueCase>{
+                             // 48 bits
+                             {false, 48, {0, 0}, 0},
+                             {false, 48, {5, 0}, 5},
+                             {false, 48, {0xfffffff8, k32ones}, uint64_t(-8)},
+                             {false, 48, {k32ones, k32ones}, uint64_t(-1)},
+                             {false, 48, {0xdcd65000, 1}, 8 * kUBillion},
+                             {true, 48, {0xfffffff8, k32ones}, uint64_t(-8)},
+                             {true, 48, {k32ones, k32ones}, uint64_t(-1)},
+                             {true, 48, {0xdcd65000, 1}, 8 * kUBillion},
+
+                             // 64 bits
+                             {false, 64, {12, 0}, 12},
+                             {false, 64, {0xdcd65000, 1}, 8 * kUBillion},
+                             {false, 48, {0xfffffff8, k32ones}, uint64_t(-8)},
+                             {false, 64, {k32ones, k32ones}, k64ones},
+                             {true, 64, {12, 0}, 12},
+                             {true, 64, {0xdcd65000, 1}, 8 * kUBillion},
+                             {true, 48, {0xfffffff8, k32ones}, uint64_t(-8)},
+                             {true, 64, {k32ones, k32ones}, k64ones},
+                         }));
+
+}  // namespace
+}  // namespace analysis
+}  // namespace opt
+}  // namespace spvtools
diff --git a/test/opt/graphics_robust_access_test.cpp b/test/opt/graphics_robust_access_test.cpp
new file mode 100644
index 0000000..137d0e8
--- /dev/null
+++ b/test/opt/graphics_robust_access_test.cpp
@@ -0,0 +1,1307 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <array>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "pass_fixture.h"
+#include "pass_utils.h"
+#include "source/opt/graphics_robust_access_pass.h"
+
+namespace {
+
+using namespace spvtools;
+
+using opt::GraphicsRobustAccessPass;
+using GraphicsRobustAccessTest = opt::PassTest<::testing::Test>;
+
+// Test incompatible module, determined at module-level.
+
+TEST_F(GraphicsRobustAccessTest, FailNotShader) {
+  const std::string text = R"(
+; CHECK: Can only process Shader modules
+OpCapability Kernel
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+TEST_F(GraphicsRobustAccessTest, FailCantProcessVariablePointers) {
+  const std::string text = R"(
+; CHECK: Can't process modules with VariablePointers capability
+OpCapability VariablePointers
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+TEST_F(GraphicsRobustAccessTest, FailCantProcessVariablePointersStorageBuffer) {
+  const std::string text = R"(
+; CHECK: Can't process modules with VariablePointersStorageBuffer capability
+OpCapability VariablePointersStorageBuffer
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+TEST_F(GraphicsRobustAccessTest, FailCantProcessRuntimeDescriptorArrayEXT) {
+  const std::string text = R"(
+; CHECK: Can't process modules with RuntimeDescriptorArrayEXT capability
+OpCapability RuntimeDescriptorArrayEXT
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+TEST_F(GraphicsRobustAccessTest, FailCantProcessPhysical32AddressingModel) {
+  const std::string text = R"(
+; CHECK: Addressing model must be Logical.  Found OpMemoryModel Physical32 OpenCL
+OpCapability Shader
+OpMemoryModel Physical32 OpenCL
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+TEST_F(GraphicsRobustAccessTest, FailCantProcessPhysical64AddressingModel) {
+  const std::string text = R"(
+; CHECK: Addressing model must be Logical.  Found OpMemoryModel Physical64 OpenCL
+OpCapability Shader
+OpMemoryModel Physical64 OpenCL
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+TEST_F(GraphicsRobustAccessTest,
+       FailCantProcessPhysicalStorageBuffer64EXTAddressingModel) {
+  const std::string text = R"(
+; CHECK: Addressing model must be Logical.  Found OpMemoryModel PhysicalStorageBuffer64EXT GLSL450
+OpCapability Shader
+OpMemoryModel PhysicalStorageBuffer64EXT GLSL450
+)";
+
+  SinglePassRunAndFail<GraphicsRobustAccessPass>(text);
+}
+
+// Test access chains
+
+// Returns the names of access chain instructions handled by the pass.
+// For the purposes of this pass, regular and in-bounds access chains are the
+// same.)
+std::vector<const char*> AccessChains() {
+  return {"OpAccessChain", "OpInBoundsAccessChain"};
+}
+
+std::string ShaderPreamble() {
+  return R"(
+  OpCapability Shader
+  OpMemoryModel Logical Simple
+  OpEntryPoint GLCompute %main "main"
+)";
+}
+
+std::string ShaderPreamble(const std::vector<std::string>& names) {
+  std::ostringstream os;
+  os << ShaderPreamble();
+  for (auto& name : names) {
+    os << "  OpName %" << name << " \"" << name << "\"\n";
+  }
+  return os.str();
+}
+
+std::string ShaderPreambleAC() {
+  return ShaderPreamble({"ac", "ptr_ty", "var"});
+}
+
+std::string ShaderPreambleAC(const std::vector<std::string>& names) {
+  auto names2 = names;
+  names2.push_back("ac");
+  names2.push_back("ptr_ty");
+  names2.push_back("var");
+  return ShaderPreamble(names2);
+}
+
+std::string DecoSSBO() {
+  return R"(
+  OpDecorate %ssbo_s BufferBlock
+  OpMemberDecorate %ssbo_s 0 Offset 0
+  OpMemberDecorate %ssbo_s 1 Offset 4
+  OpMemberDecorate %ssbo_s 2 Offset 16
+  OpDecorate %var DescriptorSet 0
+  OpDecorate %var Binding 0
+)";
+}
+
+std::string TypesVoid() {
+  return R"(
+  %void = OpTypeVoid
+  %void_fn = OpTypeFunction %void
+)";
+}
+
+std::string TypesInt() {
+  return R"(
+  %uint = OpTypeInt 32 0
+  %int = OpTypeInt 32 1
+)";
+}
+
+std::string TypesFloat() {
+  return R"(
+  %float = OpTypeFloat 32
+)";
+}
+
+std::string TypesShort() {
+  return R"(
+  %ushort = OpTypeInt 16 0
+  %short = OpTypeInt 16 1
+)";
+}
+
+std::string TypesLong() {
+  return R"(
+  %ulong = OpTypeInt 64 0
+  %long = OpTypeInt 64 1
+)";
+}
+
+std::string MainPrefix() {
+  return R"(
+  %main = OpFunction %void None %void_fn
+  %entry = OpLabel
+)";
+}
+
+std::string MainSuffix() {
+  return R"(
+  OpReturn
+  OpFunctionEnd
+)";
+}
+
+std::string ACCheck(const std::string& access_chain_inst,
+                    const std::string& original,
+                    const std::string& transformed) {
+  return "\n ; CHECK: %ac = " + access_chain_inst + " %ptr_ty %var" +
+         (transformed.size() ? " " : "") + transformed +
+         "\n ; CHECK-NOT: " + access_chain_inst +
+         "\n ; CHECK-NEXT: OpReturn"
+         "\n %ac = " +
+         access_chain_inst + " %ptr_ty %var " + (original.size() ? " " : "") +
+         original + "\n";
+}
+
+std::string ACCheckFail(const std::string& access_chain_inst,
+                        const std::string& original,
+                        const std::string& transformed) {
+  return "\n ; CHECK: %ac = " + access_chain_inst + " %ptr_ty %var" +
+         (transformed.size() ? " " : "") + transformed +
+         "\n ; CHECK-NOT: " + access_chain_inst +
+         "\n ; CHECK-NOT: OpReturn"
+         "\n %ac = " +
+         access_chain_inst + " %ptr_ty %var " + (original.size() ? " " : "") +
+         original + "\n";
+}
+
+// Access chain into:
+//   Vector
+//     Vector sizes 2, 3, 4
+//   Matrix
+//     Matrix columns 2, 4
+//     Component is vector 2, 4
+//   Array
+//   Struct
+//   TODO(dneto): RuntimeArray
+
+TEST_F(GraphicsRobustAccessTest, ACVectorLeastInboundConstantUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt() << R"(
+       %uvec2 = OpTypeVector %uint 2
+       %var_ty = OpTypePointer Function %uvec2
+       %ptr_ty = OpTypePointer Function %uint
+       %uint_0 = OpConstant %uint 0
+       )"
+            << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%uint_0", "%uint_0")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorMostInboundConstantUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt() << R"(
+       %v4uint = OpTypeVector %uint 4
+       %var_ty = OpTypePointer Function %v4uint
+       %ptr_ty = OpTypePointer Function %uint
+       %uint_3 = OpConstant %uint 3
+       )"
+            << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%uint_3", "%uint_3")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorExcessConstantClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt() << R"(
+       %v4uint = OpTypeVector %uint 4
+       %var_ty = OpTypePointer Function %v4uint
+       %ptr_ty = OpTypePointer Function %uint
+       %uint_4 = OpConstant %uint 4
+       )"
+            << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%uint_4", "%uint_3")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorNegativeConstantClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt() << R"(
+       %v4uint = OpTypeVector %uint 4
+       %var_ty = OpTypePointer Function %v4uint
+       %ptr_ty = OpTypePointer Function %uint
+       %int_n1 = OpConstant %int -1
+       )"
+            << MainPrefix() << R"(
+       ; CHECK: %int_0 = OpConstant %int 0
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%int_n1", "%int_0")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+// Like the previous test, but ensures the pass knows how to modify an index
+// which does not come first in the access chain.
+TEST_F(GraphicsRobustAccessTest, ACVectorInArrayNegativeConstantClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt() << R"(
+       %v4uint = OpTypeVector %uint 4
+       %uint_1 = OpConstant %uint 1
+       %uint_2 = OpConstant %uint 2
+       %arr = OpTypeArray %v4uint %uint_2
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %uint
+       %int_n1 = OpConstant %int -1
+       )"
+            << MainPrefix() << R"(
+       ; CHECK: %int_0 = OpConstant %int 0
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%uint_1 %int_n1", "%uint_1 %int_0") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorGeneralClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt() << R"(
+       %v4uint = OpTypeVector %uint 4
+       %var_ty = OpTypePointer Function %v4uint
+       %ptr_ty = OpTypePointer Function %uint
+       %i = OpUndef %int)"
+            << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_3 = OpConstant %int 3
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %int_3
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%i", "%[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorGeneralShortClamped) {
+  // Show that signed 16 bit integers are clamped as well.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesShort() <<
+        R"(
+       %v4short = OpTypeVector %short 4
+       %var_ty = OpTypePointer Function %v4short
+       %ptr_ty = OpTypePointer Function %short
+       %i = OpUndef %short)"
+            << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK-DAG: %short_0 = OpConstant %short 0
+       ; CHECK-DAG: %short_3 = OpConstant %short 3
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %short %[[GLSLSTD450]] UClamp %i %short_0 %short_3
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%i", "%[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorGeneralUShortClamped) {
+  // Show that unsigned 16 bit integers are clamped as well.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesShort() <<
+        R"(
+       %v4ushort = OpTypeVector %ushort 4
+       %var_ty = OpTypePointer Function %v4ushort
+       %ptr_ty = OpTypePointer Function %ushort
+       %i = OpUndef %ushort)"
+            << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK-DAG: %ushort_0 = OpConstant %ushort 0
+       ; CHECK-DAG: %ushort_3 = OpConstant %ushort 3
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %ushort %[[GLSLSTD450]] UClamp %i %ushort_0 %ushort_3
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%i", "%[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorGeneralLongClamped) {
+  // Show that signed 64 bit integers are clamped as well.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int64\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesLong() <<
+        R"(
+       %v4long = OpTypeVector %long 4
+       %var_ty = OpTypePointer Function %v4long
+       %ptr_ty = OpTypePointer Function %long
+       %i = OpUndef %long)"
+            << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK-DAG: %long_0 = OpConstant %long 0
+       ; CHECK-DAG: %long_3 = OpConstant %long 3
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %long %[[GLSLSTD450]] UClamp %i %long_0 %long_3
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%i", "%[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACVectorGeneralULongClamped) {
+  // Show that unsigned 64 bit integers are clamped as well.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int64\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesLong() <<
+        R"(
+       %v4ulong = OpTypeVector %ulong 4
+       %var_ty = OpTypePointer Function %v4ulong
+       %ptr_ty = OpTypePointer Function %ulong
+       %i = OpUndef %ulong)"
+            << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK-DAG: %ulong_0 = OpConstant %ulong 0
+       ; CHECK-DAG: %ulong_3 = OpConstant %ulong 3
+       ; CHECK-NOT: = OpTypeInt 32
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %ulong %[[GLSLSTD450]] UClamp %i %ulong_0 %ulong_3
+       %var = OpVariable %var_ty Function)" << ACCheck(ac, "%i", "%[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACMatrixLeastInboundConstantUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %v2float = OpTypeVector %float 2
+       %mat4v2float = OpTypeMatrix %v2float 4
+       %var_ty = OpTypePointer Function %mat4v2float
+       %ptr_ty = OpTypePointer Function %float
+       %uint_0 = OpConstant %uint 0
+       %uint_1 = OpConstant %uint 1
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%uint_0 %uint_1", "%uint_0 %uint_1")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACMatrixMostInboundConstantUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %v2float = OpTypeVector %float 2
+       %mat4v2float = OpTypeMatrix %v2float 4
+       %var_ty = OpTypePointer Function %mat4v2float
+       %ptr_ty = OpTypePointer Function %float
+       %uint_1 = OpConstant %uint 1
+       %uint_3 = OpConstant %uint 3
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%uint_3 %uint_1", "%uint_3 %uint_1")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACMatrixExcessConstantClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %v2float = OpTypeVector %float 2
+       %mat4v2float = OpTypeMatrix %v2float 4
+       %var_ty = OpTypePointer Function %mat4v2float
+       %ptr_ty = OpTypePointer Function %float
+       %uint_1 = OpConstant %uint 1
+       %uint_4 = OpConstant %uint 4
+       )" << MainPrefix() << R"(
+       ; CHECK: %uint_3 = OpConstant %uint 3
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%uint_4 %uint_1", "%uint_3 %uint_1")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACMatrixNegativeConstantClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %v2float = OpTypeVector %float 2
+       %mat4v2float = OpTypeMatrix %v2float 4
+       %var_ty = OpTypePointer Function %mat4v2float
+       %ptr_ty = OpTypePointer Function %float
+       %uint_1 = OpConstant %uint 1
+       %int_n1 = OpConstant %int -1
+       )" << MainPrefix() << R"(
+       ; CHECK: %int_0 = OpConstant %int 0
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%int_n1 %uint_1", "%int_0 %uint_1") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACMatrixGeneralClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %v2float = OpTypeVector %float 2
+       %mat4v2float = OpTypeMatrix %v2float 4
+       %var_ty = OpTypePointer Function %mat4v2float
+       %ptr_ty = OpTypePointer Function %float
+       %uint_1 = OpConstant %uint 1
+       %i = OpUndef %int
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_3 = OpConstant %int 3
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %int_3
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i %uint_1", "%[[clamp]] %uint_1") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayLeastInboundConstantUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %uint_200 = OpConstant %uint 200
+       %arr = OpTypeArray %float %uint_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %int_0 = OpConstant %int 0
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%int_0", "%int_0") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayMostInboundConstantUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %uint_200 = OpConstant %uint 200
+       %arr = OpTypeArray %float %uint_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %int_199 = OpConstant %int 199
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%int_199", "%int_199") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %uint_200 = OpConstant %uint 200
+       %arr = OpTypeArray %float %uint_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %int
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_199 = OpConstant %int 199
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %int_199
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralShortIndexUIntBoundsClamped) {
+  // Index is signed short, array bounds overflows the index type.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesShort() << TypesFloat() << R"(
+       %uint_70000 = OpConstant %uint 70000 ; overflows 16bits
+       %arr = OpTypeArray %float %uint_70000
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %short
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK-DAG: %uint_69999 = OpConstant %uint 69999
+       ; CHECK: OpLabel
+       ; CHECK: %[[i_ext:\w+]] = OpSConvert %uint %i
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %[[i_ext]] %uint_0 %uint_69999
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralUShortIndexIntBoundsClamped) {
+  // Index is unsigned short, array bounds overflows the index type.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesShort() << TypesFloat() << R"(
+       %int_70000 = OpConstant %int 70000 ; overflows 16bits
+       %arr = OpTypeArray %float %int_70000
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %ushort
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK-DAG: %uint_69999 = OpConstant %uint 69999
+       ; CHECK: OpLabel
+       ; CHECK: %[[i_ext:\w+]] = OpUConvert %uint %i
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %[[i_ext]] %uint_0 %uint_69999
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralUIntIndexShortBoundsClamped) {
+  // Signed int index i is wider than the array bounds type.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesShort() << TypesFloat() << R"(
+       %short_200 = OpConstant %short 200
+       %arr = OpTypeArray %float %short_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %uint
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK-DAG: %uint_199 = OpConstant %uint 199
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %i %uint_0 %uint_199
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralIntIndexUShortBoundsClamped) {
+  // Unsigned int index i is wider than the array bounds type.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesShort() << TypesFloat() << R"(
+       %ushort_200 = OpConstant %ushort 200
+       %arr = OpTypeArray %float %ushort_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %int
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_199 = OpConstant %int 199
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %int_199
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralLongIndexUIntBoundsClamped) {
+  // Signed long index i is wider than the array bounds type.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int64\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesLong() << TypesFloat() << R"(
+       %uint_200 = OpConstant %uint 200
+       %arr = OpTypeArray %float %uint_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %long
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %long_0 = OpConstant %long 0
+       ; CHECK-DAG: %long_199 = OpConstant %long 199
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %long %[[GLSLSTD450]] UClamp %i %long_0 %long_199
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayGeneralULongIndexIntBoundsClamped) {
+  // Unsigned long index i is wider than the array bounds type.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int64\n"
+            << ShaderPreambleAC({"i"}) << TypesVoid() << TypesInt()
+            << TypesLong() << TypesFloat() << R"(
+       %int_200 = OpConstant %int 200
+       %arr = OpTypeArray %float %int_200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %ulong
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %ulong_0 = OpConstant %ulong 0
+       ; CHECK-DAG: %ulong_199 = OpConstant %ulong 199
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %ulong %[[GLSLSTD450]] UClamp %i %ulong_0 %ulong_199
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%i", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArraySpecIdSizedAlwaysClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"spec200"}) << R"(
+       OpDecorate %spec200 SpecId 0 )" << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %spec200 = OpSpecConstant %int 200
+       %arr = OpTypeArray %float %spec200
+       %var_ty = OpTypePointer Function %arr
+       %ptr_ty = OpTypePointer Function %float
+       %uint_5 = OpConstant %uint 5
+       )" << MainPrefix() << R"(
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK-DAG: %uint_1 = OpConstant %uint 1
+       ; CHECK: OpLabel
+       ; CHECK: %[[max:\w+]] = OpISub %uint %spec200 %uint_1
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %uint_5 %uint_0 %[[max]]
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%uint_5", "%[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructLeastUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       %int_0 = OpConstant %int 0
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%int_0", "%int_0") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructMostUntouched) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       %int_2 = OpConstant %int 2
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function)"
+            << ACCheck(ac, "%int_2", "%int_2") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructSpecConstantFail) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"struct", "spec200"})
+            << "OpDecorate %spec200 SpecId 0\n"
+            <<
+
+        TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %spec200 = OpSpecConstant %int 200
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function
+       ; CHECK: Member index into struct is not a constant integer
+       ; CHECK-SAME: %spec200 = OpSpecConstant %int 200
+       )"
+            << ACCheckFail(ac, "%spec200", "%spec200") << MainSuffix();
+    SinglePassRunAndFail<GraphicsRobustAccessPass>(shaders.str());
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructFloatConstantFail) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"struct"}) <<
+
+        TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %float_2 = OpConstant %float 2
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function
+       ; CHECK: Member index into struct is not a constant integer
+       ; CHECK-SAME: %float_2 = OpConstant %float 2
+       )"
+            << ACCheckFail(ac, "%float_2", "%float_2") << MainSuffix();
+    SinglePassRunAndFail<GraphicsRobustAccessPass>(shaders.str());
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructNonConstantFail) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"struct", "i"}) <<
+
+        TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %float_2 = OpConstant %float 2
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpUndef %int
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function
+       ; CHECK: Member index into struct is not a constant integer
+       ; CHECK-SAME: %i = OpUndef %int
+       )"
+            << ACCheckFail(ac, "%i", "%i") << MainSuffix();
+    SinglePassRunAndFail<GraphicsRobustAccessPass>(shaders.str());
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructExcessFail) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"struct", "i"}) << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpConstant %int 4
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function
+       ; CHECK: Member index 4 is out of bounds for struct type:
+       ; CHECK-SAME: %struct = OpTypeStruct %float %float %float
+       )"
+            << ACCheckFail(ac, "%i", "%i") << MainSuffix();
+    SinglePassRunAndFail<GraphicsRobustAccessPass>(shaders.str());
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACStructNegativeFail) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"struct", "i"}) << TypesVoid() << TypesInt()
+            << TypesFloat() << R"(
+       %struct = OpTypeStruct %float %float %float
+       %var_ty = OpTypePointer Function %struct
+       %ptr_ty = OpTypePointer Function %float
+       %i = OpConstant %int -1
+       )" << MainPrefix() << R"(
+       %var = OpVariable %var_ty Function
+       ; CHECK: Member index -1 is out of bounds for struct type:
+       ; CHECK-SAME: %struct = OpTypeStruct %float %float %float
+       )"
+            << ACCheckFail(ac, "%i", "%i") << MainSuffix();
+    SinglePassRunAndFail<GraphicsRobustAccessPass>(shaders.str());
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayLeastInboundClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC() << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 "
+            << DecoSSBO() << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %uint %uint %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_0 = OpConstant %int 0
+       %int_2 = OpConstant %int 2
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK: %int_1 = OpConstant %int 1
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK: %[[max:\w+]] = OpISub %int %[[arrlen]] %int_1
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %int_0 %int_0 %[[max]]
+       )"
+            << MainPrefix() << ACCheck(ac, "%int_2 %int_0", "%int_2 %[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayGeneralShortIndexClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 " << DecoSSBO()
+            << TypesVoid() << TypesShort() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %short %short %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %short_2 = OpConstant %short 2
+       %i = OpUndef %short
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK: %uint = OpTypeInt 32 0
+       ; CHECK-DAG: %uint_1 = OpConstant %uint 1
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK-DAG: %[[max:\w+]] = OpISub %uint %[[arrlen]] %uint_1
+       ; CHECK-DAG: %[[i_ext:\w+]] = OpSConvert %uint %i
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %[[i_ext]] %uint_0 %[[max]]
+       )"
+            << MainPrefix() << ACCheck(ac, "%short_2 %i", "%short_2 %[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayGeneralUShortIndexClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int16\n"
+            << ShaderPreambleAC({"i"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 " << DecoSSBO()
+            << TypesVoid() << TypesShort() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %short %short %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %short_2 = OpConstant %short 2
+       %i = OpUndef %ushort
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK: %uint = OpTypeInt 32 0
+       ; CHECK-DAG: %uint_1 = OpConstant %uint 1
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK-DAG: %[[max:\w+]] = OpISub %uint %[[arrlen]] %uint_1
+       ; CHECK-DAG: %[[i_ext:\w+]] = OpSConvert %uint %i
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %[[i_ext]] %uint_0 %[[max]]
+       )"
+            << MainPrefix() << ACCheck(ac, "%short_2 %i", "%short_2 %[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayGeneralIntIndexClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 " << DecoSSBO()
+            << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %int
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_1 = OpConstant %int 1
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK: %[[max:\w+]] = OpISub %int %[[arrlen]] %int_1
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %[[max]]
+       )" << MainPrefix()
+            << ACCheck(ac, "%int_2 %i", "%int_2 %[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayGeneralUIntIndexClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 " << DecoSSBO()
+            << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %uint
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %uint_1 = OpConstant %uint 1
+       ; CHECK-DAG: %uint_0 = OpConstant %uint 0
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK: %[[max:\w+]] = OpISub %uint %[[arrlen]] %uint_1
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %uint %[[GLSLSTD450]] UClamp %i %uint_0 %[[max]]
+       )" << MainPrefix()
+            << ACCheck(ac, "%int_2 %i", "%int_2 %[[clamp]]") << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayGeneralLongIndexClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int64" << ShaderPreambleAC({"i"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 " << DecoSSBO()
+            << TypesVoid() << TypesInt() << TypesLong() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %long
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %long_0 = OpConstant %long 0
+       ; CHECK-DAG: %long_1 = OpConstant %long 1
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK: %[[arrlen_ext:\w+]] = OpUConvert %ulong %[[arrlen]]
+       ; CHECK: %[[max:\w+]] = OpISub %long %[[arrlen_ext]] %long_1
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %long %[[GLSLSTD450]] UClamp %i %long_0 %[[max]]
+       )"
+            << MainPrefix() << ACCheck(ac, "%int_2 %i", "%int_2 %[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayGeneralULongIndexClamped) {
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << "OpCapability Int64" << ShaderPreambleAC({"i"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 4 " << DecoSSBO()
+            << TypesVoid() << TypesInt() << TypesLong() << TypesFloat() << R"(
+       %rtarr = OpTypeRuntimeArray %float
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %ulong
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %ulong_0 = OpConstant %ulong 0
+       ; CHECK-DAG: %ulong_1 = OpConstant %ulong 1
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK: %[[arrlen_ext:\w+]] = OpUConvert %ulong %[[arrlen]]
+       ; CHECK: %[[max:\w+]] = OpISub %ulong %[[arrlen_ext]] %ulong_1
+       ; CHECK: %[[clamp:\w+]] = OpExtInst %ulong %[[GLSLSTD450]] UClamp %i %ulong_0 %[[max]]
+       )"
+            << MainPrefix() << ACCheck(ac, "%int_2 %i", "%int_2 %[[clamp]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACRTArrayStructVectorElem) {
+  // The point of this test is that the access chain can have indices past the
+  // index into the runtime array.  For good measure, the index into the final
+  // struct is out of bounds.  We have to clamp that index too.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i", "j"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 32\n"
+            << DecoSSBO() << "OpMemberDecorate %rtelem 0 Offset 0\n"
+            << "OpMemberDecorate %rtelem 1 Offset 16\n"
+            << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %v4float = OpTypeVector %float 4
+       %rtelem = OpTypeStruct %v4float %v4float
+       %rtarr = OpTypeRuntimeArray %rtelem
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %var_ty = OpTypePointer Uniform %ssbo_s
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_1 = OpConstant %int 1
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %int
+       %j = OpUndef %int
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_3 = OpConstant %int 3
+       ; CHECK: OpLabel
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %var 2
+       ; CHECK: %[[max:\w+]] = OpISub %int %[[arrlen]] %int_1
+       ; CHECK: %[[clamp_i:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %[[max]]
+       ; CHECK: %[[clamp_j:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %j %int_0 %int_3
+       )" << MainPrefix()
+            << ACCheck(ac, "%int_2 %i %int_1 %j",
+                       "%int_2 %[[clamp_i]] %int_1 %[[clamp_j]]")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACArrayRTArrayStructVectorElem) {
+  // Now add an additional level of arrays around the Block-decorated struct.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i", "ssbo_s"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 32\n"
+            << DecoSSBO() << "OpMemberDecorate %rtelem 0 Offset 0\n"
+            << "OpMemberDecorate %rtelem 1 Offset 16\n"
+            << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %v4float = OpTypeVector %float 4
+       %rtelem = OpTypeStruct %v4float %v4float
+       %rtarr = OpTypeRuntimeArray %rtelem
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %arr_size = OpConstant %int 10
+       %arr_ssbo = OpTypeArray %ssbo_s %arr_size
+       %var_ty = OpTypePointer Uniform %arr_ssbo
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_1 = OpConstant %int 1
+       %int_2 = OpConstant %int 2
+       %int_17 = OpConstant %int 17
+       %i = OpUndef %int
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %[[ssbo_p:\w+]] = OpTypePointer Uniform %ssbo_s
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_9 = OpConstant %int 9
+       ; CHECK: OpLabel
+       ; This access chain is manufatured only so we can compute the array length.
+       ; Note that the %int_9 is already clamped
+       ; CHECK: %[[ssbo_base:\w+]] = )" << ac
+            << R"( %[[ssbo_p]] %var %int_9
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %[[ssbo_base]] 2
+       ; CHECK: %[[max:\w+]] = OpISub %int %[[arrlen]] %int_1
+       ; CHECK: %[[clamp_i:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %[[max]]
+       )" << MainPrefix()
+            << ACCheck(ac, "%int_17 %int_2 %i %int_1 %int_2",
+                       "%int_9 %int_2 %[[clamp_i]] %int_1 %int_2")
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest, ACSplitACArrayRTArrayStructVectorElem) {
+  // Split the address calculation across two access chains.  Force
+  // the transform to walk up the access chains to find the base variable.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i", "j", "k", "ssbo_s", "ssbo_pty",
+                                 "rtarr_pty", "ac_ssbo", "ac_rtarr"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 32\n"
+            << DecoSSBO() << "OpMemberDecorate %rtelem 0 Offset 0\n"
+            << "OpMemberDecorate %rtelem 1 Offset 16\n"
+            << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %v4float = OpTypeVector %float 4
+       %rtelem = OpTypeStruct %v4float %v4float
+       %rtarr = OpTypeRuntimeArray %rtelem
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %arr_size = OpConstant %int 10
+       %arr_ssbo = OpTypeArray %ssbo_s %arr_size
+       %var_ty = OpTypePointer Uniform %arr_ssbo
+       %ssbo_pty = OpTypePointer Uniform %ssbo_s
+       %rtarr_pty = OpTypePointer Uniform %rtarr
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_1 = OpConstant %int 1
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %int
+       %j = OpUndef %int
+       %k = OpUndef %int
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_9 = OpConstant %int 9
+       ; CHECK-DAG: %int_3 = OpConstant %int 3
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp_i:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %int_9
+       ; CHECK: %ac_ssbo = )" << ac
+            << R"( %ssbo_pty %var %[[clamp_i]]
+       ; CHECK: %ac_rtarr = )"
+            << ac << R"( %rtarr_pty %ac_ssbo %int_2
+
+       ; This is the interesting bit.  This array length is needed for an OpAccessChain
+       ; computing %ac, but the algorithm had to track back through %ac_rtarr's
+       ; definition to find the base pointer %ac_ssbo.
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %ac_ssbo 2
+       ; CHECK: %[[max:\w+]] = OpISub %int %[[arrlen]] %int_1
+       ; CHECK: %[[clamp_j:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %j %int_0 %[[max]]
+       ; CHECK: %[[clamp_k:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %k %int_0 %int_3
+       ; CHECK: %ac = )" << ac
+            << R"( %ptr_ty %ac_rtarr %[[clamp_j]] %int_1 %[[clamp_k]]
+       ; CHECK-NOT: AccessChain
+       )" << MainPrefix()
+            << "%ac_ssbo = " << ac << " %ssbo_pty %var %i\n"
+            << "%ac_rtarr = " << ac << " %rtarr_pty %ac_ssbo %int_2\n"
+            << "%ac = " << ac << " %ptr_ty %ac_rtarr %j %int_1 %k\n"
+
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+TEST_F(GraphicsRobustAccessTest,
+       ACSplitACArrayRTArrayStructVectorElemAcrossBasicBlocks) {
+  // Split the address calculation across two access chains.  Force
+  // the transform to walk up the access chains to find the base variable.
+  // This time, put the different access chains in different basic blocks.
+  // This sanity checks that we keep the instruction-to-block mapping
+  // consistent.
+  for (auto* ac : AccessChains()) {
+    std::ostringstream shaders;
+    shaders << ShaderPreambleAC({"i", "j", "k", "bb1", "bb2", "ssbo_s",
+                                 "ssbo_pty", "rtarr_pty", "ac_ssbo",
+                                 "ac_rtarr"})
+            << "OpMemberDecorate %ssbo_s 0 ArrayStride 32\n"
+            << DecoSSBO() << "OpMemberDecorate %rtelem 0 Offset 0\n"
+            << "OpMemberDecorate %rtelem 1 Offset 16\n"
+            << TypesVoid() << TypesInt() << TypesFloat() << R"(
+       %v4float = OpTypeVector %float 4
+       %rtelem = OpTypeStruct %v4float %v4float
+       %rtarr = OpTypeRuntimeArray %rtelem
+       %ssbo_s = OpTypeStruct %int %int %rtarr
+       %arr_size = OpConstant %int 10
+       %arr_ssbo = OpTypeArray %ssbo_s %arr_size
+       %var_ty = OpTypePointer Uniform %arr_ssbo
+       %ssbo_pty = OpTypePointer Uniform %ssbo_s
+       %rtarr_pty = OpTypePointer Uniform %rtarr
+       %ptr_ty = OpTypePointer Uniform %float
+       %var = OpVariable %var_ty Uniform
+       %int_1 = OpConstant %int 1
+       %int_2 = OpConstant %int 2
+       %i = OpUndef %int
+       %j = OpUndef %int
+       %k = OpUndef %int
+       ; CHECK: %[[GLSLSTD450:\w+]] = OpExtInstImport "GLSL.std.450"
+       ; CHECK-DAG: %int_0 = OpConstant %int 0
+       ; CHECK-DAG: %int_9 = OpConstant %int 9
+       ; CHECK-DAG: %int_3 = OpConstant %int 3
+       ; CHECK: OpLabel
+       ; CHECK: %[[clamp_i:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %i %int_0 %int_9
+       ; CHECK: %ac_ssbo = )" << ac
+            << R"( %ssbo_pty %var %[[clamp_i]]
+       ; CHECK: %bb1 = OpLabel
+       ; CHECK: %ac_rtarr = )"
+            << ac << R"( %rtarr_pty %ac_ssbo %int_2
+       ; CHECK: %bb2 = OpLabel
+
+       ; This is the interesting bit.  This array length is needed for an OpAccessChain
+       ; computing %ac, but the algorithm had to track back through %ac_rtarr's
+       ; definition to find the base pointer %ac_ssbo.
+       ; CHECK: %[[arrlen:\w+]] = OpArrayLength %uint %ac_ssbo 2
+       ; CHECK: %[[max:\w+]] = OpISub %int %[[arrlen]] %int_1
+       ; CHECK: %[[clamp_j:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %j %int_0 %[[max]]
+       ; CHECK: %[[clamp_k:\w+]] = OpExtInst %int %[[GLSLSTD450]] UClamp %k %int_0 %int_3
+       ; CHECK: %ac = )" << ac
+            << R"( %ptr_ty %ac_rtarr %[[clamp_j]] %int_1 %[[clamp_k]]
+       ; CHECK-NOT: AccessChain
+       )" << MainPrefix()
+            << "%ac_ssbo = " << ac << " %ssbo_pty %var %i\n"
+            << "OpBranch %bb1\n%bb1 = OpLabel\n"
+            << "%ac_rtarr = " << ac << " %rtarr_pty %ac_ssbo %int_2\n"
+            << "OpBranch %bb2\n%bb2 = OpLabel\n"
+            << "%ac = " << ac << " %ptr_ty %ac_rtarr %j %int_1 %k\n"
+
+            << MainSuffix();
+    SinglePassRunAndMatch<GraphicsRobustAccessPass>(shaders.str(), true);
+  }
+}
+
+// TODO(dneto): Test access chain index wider than 64 bits?
+// TODO(dneto): Test struct access chain index wider than 64 bits?
+// TODO(dneto): OpImageTexelPointer
+//   - all Dim types: 1D 2D Cube 3D Rect Buffer
+//   - all Dim types that can be arrayed: 1D 2D 3D
+//   - sample index: set to 0 if not multisampled
+//   - Dim (2D, Cube Rect} with multisampling
+//      -1 0 max excess
+// TODO(dneto): Test OpImageTexelPointer with coordinate component index other
+// than 32 bits.
+
+}  // namespace
diff --git a/tools/opt/opt.cpp b/tools/opt/opt.cpp
index cce1053..8dcf933 100644
--- a/tools/opt/opt.cpp
+++ b/tools/opt/opt.cpp
@@ -206,6 +206,11 @@
                Freeze the values of specialization constants to their default
                values.)");
   printf(R"(
+  --graphics-robust-access
+               Clamp indices used to access buffers and internal composite
+               values, providing guarantees that satisfy Vulkan's
+               robustBufferAccess rules.)");
+  printf(R"(
   --generate-webgpu-initializers
                Adds initial values to OpVariable instructions that are missing
                them, due to their storage type requiring them for WebGPU.)");