Enable Integer Range Analysis on `Access` in Robustness Transform

This patch adds the support of Integer Range Analysis in Robustness
transform behind the `RobustnessConfig.use_integer_range_analysis`
option and uses Integer Range Analysis on the `Access` expression to
an array.

Bug: 348701956
Test: tint_unittests
Change-Id: Id6976e7fc04704324d776fd184238fb88036db62
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/242975
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Jiawei Shao <jiawei.shao@intel.com>
diff --git a/src/tint/lang/core/ir/transform/robustness.cc b/src/tint/lang/core/ir/transform/robustness.cc
index 73c41ec..9d716c9 100644
--- a/src/tint/lang/core/ir/transform/robustness.cc
+++ b/src/tint/lang/core/ir/transform/robustness.cc
@@ -28,7 +28,9 @@
 #include "src/tint/lang/core/ir/transform/robustness.h"
 
 #include <algorithm>
+#include <optional>
 
+#include "src/tint/lang/core/ir/analysis/integer_range_analysis.h"
 #include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/validator.h"
@@ -57,6 +59,9 @@
     /// The type manager.
     core::type::Manager& ty{ir.Types()};
 
+    /// For integer range analysis when needed
+    std::optional<ir::analysis::IntegerRangeAnalysis> integer_range_analysis = {};
+
     /// Process the module.
     void Process() {
         // Find the access instructions that may need to be clamped.
@@ -65,6 +70,11 @@
         Vector<ir::StoreVectorElement*, 64> vector_stores;
         Vector<ir::CoreBuiltinCall*, 64> subgroup_matrix_calls;
         Vector<ir::CoreBuiltinCall*, 64> texture_calls;
+
+        if (config.use_integer_range_analysis) {
+            integer_range_analysis.emplace(&ir);
+        }
+
         for (auto* inst : ir.Instructions()) {
             tint::Switch(
                 inst,  //
@@ -215,6 +225,49 @@
         inst->SetOperand(op_idx, clamped_idx);
     }
 
+    /// Check if operand @p idx may be less than 0 or greater than @p limit with integer range
+    /// analysis algorithm.
+    /// @param idx the index to check
+    /// @param limit the upper limit @idx to compare with.
+    /// @returns true when @idx may be out of bound, false otherwise
+    bool IndexMayOutOfBound(ir::Value* idx, ir::Value* limit) {
+        // Return true when integer range analysis is disabled.
+        if (!integer_range_analysis.has_value()) {
+            return true;
+        }
+
+        // Return true when `limit` is not a constant value.
+        auto* const_limit = limit->As<ir::Constant>();
+        if (!const_limit) {
+            return true;
+        }
+
+        // Return true when we cannot get a valid range for `idx`.
+        const auto* integer_range = integer_range_analysis->GetInfo(idx);
+        if (!integer_range) {
+            return true;
+        }
+
+        TINT_ASSERT(const_limit->Value()->Type()->Is<type::U32>());
+        uint32_t const_limit_value = const_limit->Value()->ValueAs<uint32_t>();
+
+        using SignedIntegerRange = ir::analysis::IntegerRangeInfo::SignedIntegerRange;
+        using UnsignedIntegerRange = ir::analysis::IntegerRangeInfo::UnsignedIntegerRange;
+
+        // Return true when `idx` may be negative or the upper bound of `idx` is greater than
+        // `limit`.
+        if (std::holds_alternative<UnsignedIntegerRange>(integer_range->range)) {
+            UnsignedIntegerRange range = std::get<UnsignedIntegerRange>(integer_range->range);
+            return range.max_bound > static_cast<uint64_t>(const_limit_value);
+        } else {
+            SignedIntegerRange range = std::get<SignedIntegerRange>(integer_range->range);
+            if (range.min_bound < 0) {
+                return true;
+            }
+            return range.max_bound > static_cast<int64_t>(const_limit_value);
+        }
+    }
+
     /// Clamp the indices of an access instruction to ensure they are within the limits of the types
     /// that they are indexing into.
     /// @param access the access instruction
@@ -262,7 +315,7 @@
                 });
 
             // If there's a dynamic limit that needs enforced, clamp the index operand.
-            if (limit) {
+            if (limit && IndexMayOutOfBound(idx, limit)) {
                 ClampOperand(access, ir::Access::kIndicesOperandOffset + i, limit);
             }
 
diff --git a/src/tint/lang/core/ir/transform/robustness.h b/src/tint/lang/core/ir/transform/robustness.h
index 4371a80..2f0044a 100644
--- a/src/tint/lang/core/ir/transform/robustness.h
+++ b/src/tint/lang/core/ir/transform/robustness.h
@@ -72,6 +72,9 @@
     /// Should the transform skip index clamping on runtime-sized arrays?
     bool disable_runtime_sized_array_index_clamping = false;
 
+    /// Should the integer range analysis be used before doing index clamping?
+    bool use_integer_range_analysis = false;
+
     /// Reflection for this class
     TINT_REFLECT(RobustnessConfig,
                  clamp_value,
@@ -84,7 +87,8 @@
                  clamp_workgroup,
                  predicate_subgroup_matrix,
                  bindings_ignored,
-                 disable_runtime_sized_array_index_clamping);
+                 disable_runtime_sized_array_index_clamping,
+                 use_integer_range_analysis);
 };
 
 /// Robustness is a transform that prevents out-of-bounds memory accesses.
diff --git a/src/tint/lang/core/ir/transform/robustness_test.cc b/src/tint/lang/core/ir/transform/robustness_test.cc
index 120bc54..f07d30e 100644
--- a/src/tint/lang/core/ir/transform/robustness_test.cc
+++ b/src/tint/lang/core/ir/transform/robustness_test.cc
@@ -61,6 +61,8 @@
 }
 using IR_BindingVariableRobustnessTest = TransformTestWithParam<BindingVariableCase>;
 
+using IR_RobustnessWithIntegerRangeAnalysisTest = TransformTest;
+
 ////////////////////////////////////////////////////////////////
 // These tests use the function address space.
 // Test clamping of vectors, matrices, and fixed-size arrays.
@@ -4171,6 +4173,808 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_MaxBound_Equal_Limit) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0u
+            idx = b.Var("idx", 0_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 20u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 20_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0u, 19u]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<u32>(b.Load(idx), 1_u));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 0u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:u32 = load %idx
+        %5:bool = lt %4, 20u
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:u32 = load %idx
+        %7:ptr<function, u32, read_write> = access %arr, %6
+        %8:u32 = load %7
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %9:u32 = load %idx
+        %10:u32 = add %9, 1u
+        store %idx, %10
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_MaxBound_LessThan_Limit) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0u
+            idx = b.Var("idx", 0_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 19u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 19_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0u, 18u]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<u32>(b.Load(idx), 1_u));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 0u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:u32 = load %idx
+        %5:bool = lt %4, 19u
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:u32 = load %idx
+        %7:ptr<function, u32, read_write> = access %arr, %6
+        %8:u32 = load %7
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %9:u32 = load %idx
+        %10:u32 = add %9, 1u
+        store %idx, %10
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_MaxBound_GreaterThan_Limit) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0u
+            idx = b.Var("idx", 0_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 21u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 21_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0, 20u]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<u32>(b.Load(idx), 1_u));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 0u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:u32 = load %idx
+        %5:bool = lt %4, 21u
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:u32 = load %idx
+        %7:u32 = min %6, 19u
+        %8:ptr<function, u32, read_write> = access %arr, %7
+        %9:u32 = load %8
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %10:u32 = load %idx
+        %11:u32 = add %10, 1u
+        store %idx, %11
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_IndexNoRange) {
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", ty.u32());
+    func->AppendParam(param);
+    b.Append(func->Block(), [&] {
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, param);
+        b.Load(access_arr);
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    %4:u32 = min %param, 19u
+    %5:ptr<function, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithExpression_MaxBound_Equal_Limit) {
+    auto* func = b.ComputeFunction("my_func", 4_u, 1_u, 1_u);
+    auto* local_invocation_id = b.FunctionParam("local_id", mod.Types().vec3<u32>());
+    local_invocation_id->SetBuiltin(tint::core::BuiltinValue::kLocalInvocationId);
+    func->SetParams({local_invocation_id});
+
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+
+        // factor = 4
+        auto* factor = b.Constant(4_u);
+        // arr = array<u32, 32>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 32>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0
+            idx = b.Var("idx", 0_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 8
+            auto* binary = b.LessThan<bool>(b.Load(idx), 8_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            // access_idx = idx * factor + local_id.x
+            // access_idx: [0, 31] (idx: [0, 7], factor = 4, local_id.x: [0, 3])
+            auto* load_idx = b.Load(idx);
+            auto* multiply = b.Multiply<u32>(load_idx, factor);
+            auto* access_local_id_x = b.Access(ty.u32(), local_invocation_id, 0_u);
+            auto* access_idx = b.Add<u32>(multiply, access_local_id_x);
+
+            // access_arr = arr[access_idx]
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, access_idx);
+            b.Load(access_arr);
+
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<u32>(b.Load(idx), 1_u));
+            b.NextIteration(loop);
+        });
+
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%my_func = @compute @workgroup_size(4u, 1u, 1u) func(%local_id:vec3<u32> [@local_invocation_id]):void {
+  $B1: {
+    %arr:ptr<function, array<u32, 32>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 0u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %5:u32 = load %idx
+        %6:bool = lt %5, 8u
+        if %6 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %7:u32 = load %idx
+        %8:u32 = mul %7, 4u
+        %9:u32 = access %local_id, 0u
+        %10:u32 = add %8, %9
+        %11:ptr<function, u32, read_write> = access %arr, %10
+        %12:u32 = load %11
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %13:u32 = load %idx
+        %14:u32 = add %13, 1u
+        store %idx, %14
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_I32_NegativeMinBound) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = -1
+            idx = b.Var("idx", -1_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 20
+            auto* binary = b.LessThan<bool>(b.Load(idx), 20_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [-1, 19]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<i32>(b.Load(idx), 1_i));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var -1i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:i32 = load %idx
+        %5:bool = lt %4, 20i
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:i32 = load %idx
+        %7:u32 = convert %6
+        %8:u32 = min %7, 19u
+        %9:ptr<function, u32, read_write> = access %arr, %8
+        %10:u32 = load %9
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %11:i32 = load %idx
+        %12:i32 = add %11, 1i
+        store %idx, %12
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_MaxBound_Equal_Limit_I32) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0
+            idx = b.Var("idx", 0_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 20
+            auto* binary = b.LessThan<bool>(b.Load(idx), 20_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0, 19]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<i32>(b.Load(idx), 1_i));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var 0i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:i32 = load %idx
+        %5:bool = lt %4, 20i
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:i32 = load %idx
+        %7:ptr<function, u32, read_write> = access %arr, %6
+        %8:u32 = load %7
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %9:i32 = load %idx
+        %10:i32 = add %9, 1i
+        store %idx, %10
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest,
+       AccessArrayWithIndex_MaxBound_LessThan_Limit_I32) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0
+            idx = b.Var("idx", 0_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 19
+            auto* binary = b.LessThan<bool>(b.Load(idx), 19_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0, 18]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<i32>(b.Load(idx), 1_i));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var 0i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:i32 = load %idx
+        %5:bool = lt %4, 19i
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:i32 = load %idx
+        %7:ptr<function, u32, read_write> = access %arr, %6
+        %8:u32 = load %7
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %9:i32 = load %idx
+        %10:i32 = add %9, 1i
+        store %idx, %10
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest,
+       AccessArrayWithIndex_MaxBound_GreaterThan_Limit_I32) {
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0
+            idx = b.Var("idx", 0_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 21
+            auto* binary = b.LessThan<bool>(b.Load(idx), 21_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0, 20]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<i32>(b.Load(idx), 1_i));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func():void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var 0i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:i32 = load %idx
+        %5:bool = lt %4, 21i
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:i32 = load %idx
+        %7:u32 = convert %6
+        %8:u32 = min %7, 19u
+        %9:ptr<function, u32, read_write> = access %arr, %8
+        %10:u32 = load %9
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %11:i32 = load %idx
+        %12:i32 = add %11, 1i
+        store %idx, %12
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_IndexNoRange_I32) {
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", ty.i32());
+    func->AppendParam(param);
+    b.Append(func->Block(), [&] {
+        // arr = array<u32, 20>
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 20>()));
+        auto* access_arr = b.Access(ty.ptr<function, u32>(), arr, param);
+        b.Load(access_arr);
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    %arr:ptr<function, array<u32, 20>, read_write> = var undef
+    %4:u32 = convert %param
+    %5:u32 = min %4, 19u
+    %6:ptr<function, u32, read_write> = access %arr, %5
+    %7:u32 = load %6
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_RobustnessWithIntegerRangeAnalysisTest, AccessArrayWithIndex_DynamicSizedArray) {
+    // arr = array<u32>
+    auto* arr = b.Var("arr", ty.ptr(storage, ty.array<u32>()));
+    arr->SetBindingPoint(0, 0);
+    mod.root_block->Append(arr);
+
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        Var* idx = nullptr;
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 0u
+            idx = b.Var("idx", 0_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 20u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 20_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+            // access_arr = arr[idx]
+            // idx: [0u, 19u]
+            auto* load_idx = b.Load(idx);
+            auto* access_arr = b.Access(ty.ptr<storage, u32>(), arr, load_idx);
+            b.Load(access_arr);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            // idx++
+            b.Store(idx, b.Add<u32>(b.Load(idx), 1_u));
+            b.NextIteration(loop);
+        });
+        b.Return(func);
+    });
+
+    auto* expect = R"(
+$B1: {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%func = func():void {
+  $B2: {
+    loop [i: $B3, b: $B4, c: $B5] {  # loop_1
+      $B3: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 0u
+        next_iteration  # -> $B4
+      }
+      $B4: {  # body
+        %4:u32 = load %idx
+        %5:bool = lt %4, 20u
+        if %5 [t: $B6, f: $B7] {  # if_1
+          $B6: {  # true
+            exit_if  # if_1
+          }
+          $B7: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        %6:u32 = load %idx
+        %7:u32 = arrayLength %arr
+        %8:u32 = sub %7, 1u
+        %9:u32 = min %6, %8
+        %10:ptr<storage, u32, read_write> = access %arr, %9
+        %11:u32 = load %10
+        continue  # -> $B5
+      }
+      $B5: {  # continuing
+        %12:u32 = load %idx
+        %13:u32 = add %12, 1u
+        store %idx, %13
+        next_iteration  # -> $B4
+      }
+    }
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = true;
+    cfg.clamp_storage = true;
+    cfg.use_integer_range_analysis = true;
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
 INSTANTIATE_TEST_SUITE_P(, IR_RobustnessTest, testing::Values(false, true));
 
 INSTANTIATE_TEST_SUITE_P(,