Range Analysis: Support step > 1 in for-loops when index is increasing

This patch implements the computations of the range on the
loop control variable which is increasing with a constant
integer step that is greater than 1.

For example:

// Range of i is [2u, 6u]
for (var i = 2u; i <= 6u; i += 2u) {
  // ...
}

// Range of i is [2, 8]
for (var i = 2; i <= 10; i += 3) {
  // ...
}

Bug: 348701956
Change-Id: I0a7d0b837718002a42419481d69b82c54448ee91
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/337495
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Shao, Jiawei <jiawei.shao@intel.com>
diff --git a/src/tint/lang/core/ir/analysis/integer_range_analysis.cc b/src/tint/lang/core/ir/analysis/integer_range_analysis.cc
index c3c6e80..e2e4846 100644
--- a/src/tint/lang/core/ir/analysis/integer_range_analysis.cc
+++ b/src/tint/lang/core/ir/analysis/integer_range_analysis.cc
@@ -75,6 +75,16 @@
     return false;
 }
 
+bool IsPositiveConstantInteger(const Value* v) {
+    if (auto* cv = v->As<Constant>()) {
+        return Switch(
+            cv->Type(), [&](const core::type::I32*) { return cv->Value()->ValueAs<int32_t>() > 0; },
+            [&](const core::type::U32*) { return cv->Value()->ValueAs<uint32_t>() > 0; },
+            [&](const Default) -> bool { return false; });
+    }
+    return false;
+}
+
 int64_t GetValueFromConstant(const Constant* value) {
     // Return an int64_t is enough as the type of `value` can only be either i32 or u32.
     [[maybe_unused]] bool is_i32_or_u32 = value->Type()->IsAnyOf<type::I32, type::U32>();
@@ -357,30 +367,73 @@
             init_max = static_cast<int64_t>(range.max_bound);
         }
 
-        // for (...; i++) or for(...; i--)
+        // for (...; i += step) or for(...; i--) where `step` is a constant positive integer.
         bool index_is_increasing = update->Op() == BinaryOp::kAdd;
 
         switch (compare_info.op) {
             case BinaryOp::kLessThanEqual: {
-                // for (var index = init_value; index <= const_rhs; index++), where `init_value` has
-                // a range `[init_min, init_max]`, then range of `index` will be:
+                // for (var index = init_value; index <= const_rhs; index += step), where
+                // `init_value` has a range `[init_min, init_max]` and `step` is a constant positive
+                // integer, then the range of `index` will be:
                 // - When `const_rhs < init_min`, the loop body can never be executed.
-                // - When `init_min <= const_rhs <= init_max`, the range of `index` is
-                //   `[init_min, const_rhs]` when `init_min <= init_value <= const_rhs`, and becomes
-                //   invalid when `init_value > const_rhs` as the loop won't be executed, so the
-                //   range of `index` is `[init_min, const_rhs]`.
-                // - When `const_rhs > init_max`, the range of `index` is `[init_min, const_rhs]` as
-                //   the loop can always be executed for all the values of `init_value`.
-                // Note that in `GetBinaryToCompareLoopControlVariableInLoopBody()` we disallow
-                // `const_rhs` to be the maximum value of `i32` or `u32`, so:
-                // - `index + 1` won't be overflow as `init_min` cannot be greater than `const_rhs`.
-                // - `index <= const_rhs` can correctly exit when `init_min + 1` is the maximum
-                //   value of `i32` or `u32`.
-                if (index_is_increasing && init_min <= compare_info.const_rhs) {
-                    IntegerRangeInfo range_info =
-                        ToIntegerRangeInfo(is_signed, init_min, compare_info.const_rhs);
-                    integer_var_range_info_map_.Add(index, range_info);
+                // - When `const_rhs >= init_min`, the loop body will only be executed when
+                //   `init_value <= const_rhs`, so the range of `index` must be the subrange in
+                //   `[init_min, const_rhs]`. For a given `init_value`, the maximum value of `index`
+                //   is `init_value + (const_rhs - init_value) / step * step`, so the maximum value
+                //   of `index` is the maximum of this expression over all the `init_value` in
+                //   `[init_min, min(init_max, const_rhs)]`:
+                //   - Let `r = (const_rhs - init_min) % step`. If `init_min + r` is in the range
+                //     `[init_min, min(init_max, const_rhs)]`, `index` can reach `const_rhs` exactly
+                //     when `init_value == init_min + r`.
+                //   - Otherwise `(const_rhs - init_value) % step` decreases as `init_value`
+                //     increases in this range, so the maximum is reached when `init_value` is
+                //     `min(init_max, const_rhs)`.
+                //   For example, for (var index = init_value; index <= 16; index += 6)
+                //   - If `init_value` is in range `[1, 7]`, then `r = (16 - 1) % 6 = 3`.
+                //     `init_min + r = 1 + 3 = 4` is in the range `[1, 7]`, so `index` can reach
+                //     `16` exactly when `init_value == 4` (`4 + 6 * 2 = 16`).
+                //   - If `init_value` is in range `[1, 2]`, then `r = (16 - 1) % 6 = 3`.
+                //     `init_min + r = 1 + 3 = 4` is not in the range `[1, 2]`, so the maximum value
+                //     of `index` (14) is reached when `init_value` is `2` (`2 + 6 * 2 = 14`).
+                if (!index_is_increasing || init_min > compare_info.const_rhs) {
+                    break;
                 }
+                const Constant* const_step = update->RHS()->As<Constant>();
+                if (const_step == nullptr) {
+                    const_step = update->LHS()->As<Constant>();
+                }
+                TINT_ASSERT(const_step);
+                int64_t step = GetValueFromConstant(const_step);
+                TINT_ASSERT(step > 0);
+
+                // The maximum value of the init value inside loop body is the minimum of `init_max`
+                // and `const_rhs`.
+                int64_t max_init_in_loop = std::min(init_max, compare_info.const_rhs);
+                int64_t remainder = (compare_info.const_rhs - init_min) % step;
+                int64_t index_max = 0;
+                if (max_init_in_loop - init_min >= remainder) {
+                    index_max = compare_info.const_rhs;
+                } else {
+                    index_max = max_init_in_loop +
+                                (compare_info.const_rhs - max_init_in_loop) / step * step;
+                }
+
+                // `index_max + step` must not exceed the maximum value of `i32` or `u32`. Otherwise
+                // `index` may be overflow and still satisfy `index <= const_rhs`, then `index` can
+                // take values outside of `[init_min, index_max]` and the loop may never exit, which
+                // is not supported in our implementation right now.
+                int64_t max_value = 0;
+                if (is_signed) {
+                    max_value = static_cast<int64_t>(i32::kHighestValue);
+                } else {
+                    max_value = static_cast<int64_t>(u32::kHighestValue);
+                }
+                if (index_max + step > max_value) {
+                    break;
+                }
+
+                IntegerRangeInfo range_info = ToIntegerRangeInfo(is_signed, init_min, index_max);
+                integer_var_range_info_map_.Add(index, range_info);
                 break;
             }
             case BinaryOp::kGreaterThanEqual: {
@@ -459,7 +512,8 @@
     // Currently we only support the loop continuing of a simple for-loop, which only has 4
     // instructions
     /// - The first instruction is to load the loop control variable into a temporary variable.
-    /// - The second instruction is to add one or minus one to the temporary variable.
+    /// - The second instruction is to add a constant positive integer to the temporary variable or
+    ///   to subtract one from the temporary variable.
     /// - The third instruction is to store the value of the temporary variable into the loop
     ///   control variable.
     /// - The fourth instruction is `next_iteration`.
@@ -489,9 +543,10 @@
         }
 
         // 2nd instruction:
-        // %dst = add %src, 1
-        // or %dst = add 1, %src
+        // %dst = add %src, step
+        // or %dst = add step, %src
         // or %dst = sub %src, 1
+        // where `step` is a constant positive integer.
         const auto* add_or_sub_from_loop_control_variable =
             load_from_loop_control_variable->next->As<Binary>();
         if (!add_or_sub_from_loop_control_variable) {
@@ -502,12 +557,12 @@
         const auto* rhs = add_or_sub_from_loop_control_variable->RHS();
         switch (add_or_sub_from_loop_control_variable->Op()) {
             case BinaryOp::kAdd: {
-                // %dst = add %src, 1
-                if (lhs == src && IsOne(rhs)) {
+                // %dst = add %src, step
+                if (lhs == src && IsPositiveConstantInteger(rhs)) {
                     break;
                 }
-                // %dst = add 1, %src
-                if (rhs == src && IsOne(lhs)) {
+                // %dst = add step, %src
+                if (rhs == src && IsPositiveConstantInteger(lhs)) {
                     break;
                 }
                 return nullptr;
diff --git a/src/tint/lang/core/ir/analysis/integer_range_analysis.h b/src/tint/lang/core/ir/analysis/integer_range_analysis.h
index c094b3e..dbbdf2f 100644
--- a/src/tint/lang/core/ir/analysis/integer_range_analysis.h
+++ b/src/tint/lang/core/ir/analysis/integer_range_analysis.h
@@ -160,7 +160,8 @@
     /// continuing block of the given loop if the loop meets the below requirements.
     /// - There are only 4 instructions in the loop initializer block.
     /// - The first instruction is to load the loop control variable into a temporary variable.
-    /// - The second instruction is to add one or minus one to the temporary variable.
+    /// - The second instruction is to add a constant positive integer to the temporary variable,
+    ///   or to minus one from the temporary variable.
     /// - The third instruction is to store the value of the temporary variable into the loop
     ///   control variable.
     /// - The fourth instruction is `next_iteration`.
diff --git a/src/tint/lang/core/ir/analysis/integer_range_analysis_test.cc b/src/tint/lang/core/ir/analysis/integer_range_analysis_test.cc
index 42c5b10..95ea99d 100644
--- a/src/tint/lang/core/ir/analysis/integer_range_analysis_test.cc
+++ b/src/tint/lang/core/ir/analysis/integer_range_analysis_test.cc
@@ -1233,9 +1233,10 @@
               analysis.GetBinaryToUpdateLoopControlVariableInContinuingBlockForTest(loop, idx));
 }
 
-TEST_F(IR_IntegerRangeAnalysisTest, AnalyzeLoopContinuing_Failure_AddTwo) {
+TEST_F(IR_IntegerRangeAnalysisTest, AnalyzeLoopContinuing_Success_AddTwo) {
     Var* idx = nullptr;
     Loop* loop = nullptr;
+    Binary* binary = nullptr;
     auto* func = b.Function("func", ty.void_());
     b.Append(func->Block(), [&] {
         loop = b.Loop();
@@ -1246,7 +1247,8 @@
         b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
         b.Append(loop->Continuing(), [&] {
             // idx += 2u;
-            b.Store(idx, b.Add(b.Load(idx), 2_u));
+            binary = b.Add(b.Load(idx), 2_u)->AsInstruction<Binary>();
+            b.Store(idx, binary);
             b.NextIteration(loop);
         });
         b.Return(func);
@@ -1279,7 +1281,7 @@
 
     IntegerRangeAnalysis analysis(&mod);
     EXPECT_EQ(idx, analysis.GetLoopControlVariableFromConstantInitializerForTest(loop));
-    EXPECT_EQ(nullptr,
+    EXPECT_EQ(binary,
               analysis.GetBinaryToUpdateLoopControlVariableInContinuingBlockForTest(loop, idx));
 }
 
@@ -18767,5 +18769,407 @@
     EXPECT_FALSE(analysis.GetInfo(j).IsValid());
 }
 
+class IR_IntegerRangeAnalysisLoopStepTest : public IR_IntegerRangeAnalysisTest {
+  protected:
+    /// Builds `for (var idx = init; idx <= rhs; idx += step)` (`idx < rhs` when `!inclusive`,
+    /// `idx = step + idx` when `step_is_lhs`) and returns `idx`.
+    template <typename T>
+    Var* BuildIncreasingLoop(T init,
+                             T rhs,
+                             T step,
+                             bool inclusive = true,
+                             bool step_is_lhs = false) {
+        Var* idx = nullptr;
+        auto* func = b.Function("func", ty.void_());
+        b.Append(func->Block(), [&] {
+            auto* loop = b.Loop();
+            b.Append(loop->Initializer(), [&] {
+                idx = b.Var("idx", b.Constant(init));
+                b.NextIteration(loop);
+            });
+            b.Append(loop->Body(), [&] {
+                auto* cmp = inclusive ? b.LessThanEqual(b.Load(idx), b.Constant(rhs))
+                                      : b.LessThan(b.Load(idx), b.Constant(rhs));
+                auto* ifelse = b.If(cmp);
+                b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+                b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+                b.Continue(loop);
+            });
+            b.Append(loop->Continuing(), [&] {
+                auto* load = b.Load(idx);
+                auto* sum =
+                    step_is_lhs ? b.Add(b.Constant(step), load) : b.Add(load, b.Constant(step));
+                b.Store(idx, sum);
+                b.NextIteration(loop);
+            });
+            b.Return(func);
+        });
+        EXPECT_EQ(Validate(mod), Success);
+        return idx;
+    }
+
+    /// Builds `for (var i = outer_init; i <= outer_rhs; i += outer_step) {
+    ///   for (var j = i; j < inner_rhs; j += inner_step) {}
+    /// }` and returns `i` and `j`.
+    template <typename T>
+    std::pair<Var*, Var*> BuildNestedLoops(T outer_init,
+                                           T outer_rhs,
+                                           T outer_step,
+                                           T inner_rhs,
+                                           T inner_step) {
+        Var* i = nullptr;
+        Var* j = nullptr;
+        auto* func = b.Function("func", ty.void_());
+        b.Append(func->Block(), [&] {
+            auto* outer = b.Loop();
+            b.Append(outer->Initializer(), [&] {
+                i = b.Var("i", b.Constant(outer_init));
+                b.NextIteration(outer);
+            });
+            b.Append(outer->Body(), [&] {
+                auto* outer_if = b.If(b.LessThanEqual(b.Load(i), b.Constant(outer_rhs)));
+                b.Append(outer_if->True(), [&] { b.ExitIf(outer_if); });
+                b.Append(outer_if->False(), [&] { b.ExitLoop(outer); });
+
+                auto* inner = b.Loop();
+                b.Append(inner->Initializer(), [&] {
+                    j = b.Var("j", b.Load(i));
+                    b.NextIteration(inner);
+                });
+                b.Append(inner->Body(), [&] {
+                    auto* inner_if = b.If(b.LessThan(b.Load(j), b.Constant(inner_rhs)));
+                    b.Append(inner_if->True(), [&] { b.ExitIf(inner_if); });
+                    b.Append(inner_if->False(), [&] { b.ExitLoop(inner); });
+                    b.Continue(inner);
+                });
+                b.Append(inner->Continuing(), [&] {
+                    b.Store(j, b.Add(b.Load(j), b.Constant(inner_step)));
+                    b.NextIteration(inner);
+                });
+                b.Continue(outer);
+            });
+            b.Append(outer->Continuing(), [&] {
+                b.Store(i, b.Add(b.Load(i), b.Constant(outer_step)));
+                b.NextIteration(outer);
+            });
+            b.Return(func);
+        });
+        EXPECT_EQ(Validate(mod), Success);
+        return {i, j};
+    }
+
+    void ExpectSigned(Var* idx, int64_t min, int64_t max) {
+        IntegerRangeAnalysis analysis(&mod);
+        const IntegerRangeInfo& info = analysis.GetInfo(idx);
+        ASSERT_TRUE(info.IsValid());
+        ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info.range));
+        const auto& range = std::get<IntegerRangeInfo::SignedIntegerRange>(info.range);
+        EXPECT_EQ(min, range.min_bound);
+        EXPECT_EQ(max, range.max_bound);
+    }
+
+    void ExpectUnsigned(Var* idx, uint64_t min, uint64_t max) {
+        IntegerRangeAnalysis analysis(&mod);
+        const IntegerRangeInfo& info = analysis.GetInfo(idx);
+        ASSERT_TRUE(info.IsValid());
+        ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info.range));
+        const auto& range = std::get<IntegerRangeInfo::UnsignedIntegerRange>(info.range);
+        EXPECT_EQ(min, range.min_bound);
+        EXPECT_EQ(max, range.max_bound);
+    }
+
+    void ExpectNoRange(Var* idx) {
+        IntegerRangeAnalysis analysis(&mod);
+        EXPECT_FALSE(analysis.GetInfo(idx).IsValid());
+    }
+};
+
+// for (var idx = 2u; idx <= 6u; idx += 2u)
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_ReachRHS_u32) {
+    Var* idx = BuildIncreasingLoop(2_u, 6_u, 2_u);
+    ExpectUnsigned(idx, 2u, 6u);
+}
+
+// for (var idx = 2; idx <= 10; idx += 3)
+// `idx` takes 2, 5, 8.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_NotReachRHS_i32) {
+    Var* idx = BuildIncreasingLoop(2_i, 10_i, 3_i);
+    ExpectSigned(idx, 2, 8);
+}
+
+// for (var idx = 0; idx < 10; idx += 3)
+// `idx` takes 0, 3, 6, 9.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_LessThan_ReachRHS_i32) {
+    Var* idx = BuildIncreasingLoop(0_i, 10_i, 3_i, /* inclusive */ false);
+    ExpectSigned(idx, 0, 9);
+}
+
+// for (var idx = 0u; idx < 10u; idx += 4u)
+// `idx` takes 0u, 4u, 8u.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_LessThan_NotReachRHS_u32) {
+    Var* idx = BuildIncreasingLoop(0_u, 10_u, 4_u, /* inclusive */ false);
+    ExpectUnsigned(idx, 0u, 8u);
+}
+
+// for (var idx = 1; idx <= 7; idx = 2 + idx)
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_StepIsLHS_i32) {
+    Var* idx = BuildIncreasingLoop(1_i, 7_i, 2_i, /* inclusive */ true, /* step_is_lhs */ true);
+    ExpectSigned(idx, 1, 7);
+}
+
+// for (var idx = 1u; idx < 8u; idx = 3u + idx)
+// `idx` takes 1u, 4u, 7u.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_StepIsLHS_LessThan_u32) {
+    Var* idx = BuildIncreasingLoop(1_u, 8_u, 3_u, /* inclusive */ false, /* step_is_lhs */ true);
+    ExpectUnsigned(idx, 1u, 7u);
+}
+
+// for (var idx = 0; idx <= 5; idx += 1)
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_StepIsOne_i32) {
+    Var* idx = BuildIncreasingLoop(0_i, 5_i, 1_i);
+    ExpectSigned(idx, 0, 5);
+}
+
+// for (var idx = 3u; idx <= 5u; idx += 10u)
+// `idx` only takes 3u.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_StepGreaterThanRange_u32) {
+    Var* idx = BuildIncreasingLoop(3_u, 5_u, 10_u);
+    ExpectUnsigned(idx, 3u, 3u);
+}
+
+// for (var idx = 5; idx <= 5; idx += 3)
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_InitEqualsRHS_i32) {
+    Var* idx = BuildIncreasingLoop(5_i, 5_i, 3_i);
+    ExpectSigned(idx, 5, 5);
+}
+
+// for (var idx = 6; idx <= 5; idx += 3)
+// The loop body is never executed.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_InitGreaterThanRHS_i32) {
+    Var* idx = BuildIncreasingLoop(6_i, 5_i, 3_i);
+    ExpectNoRange(idx);
+}
+
+// for (var idx = -10; idx <= -1; idx += 4)
+// `idx` takes -10, -6, -2.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Negative_i32) {
+    Var* idx = BuildIncreasingLoop(-10_i, -1_i, 4_i);
+    ExpectSigned(idx, -10, -2);
+}
+
+// for (var idx = kMax - 9; idx <= kMax - 3; idx += 3), where `kMax` is the maximum i32.
+// `idx` takes `kMax - 9`, `kMax - 6`, `kMax - 3`, and `(kMax - 3) + 3` doesn't overflow.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_MaxPlusStepIsHighestValue_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(i32(kMax - 9), i32(kMax - 3), 3_i);
+    ExpectSigned(idx, kMax - 9, kMax - 3);
+}
+
+// for (var idx = kMax - 8; idx <= kMax - 2; idx += 3), where `kMax` is the maximum i32.
+// `idx` takes `kMax - 8`, `kMax - 5`, `kMax - 2`, and `(kMax - 2) + 3` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_Overflow_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(i32(kMax - 8), i32(kMax - 2), 3_i);
+    ExpectNoRange(idx);
+}
+
+// for (var idx = kMax - 4; idx < kMax; idx += 2), where `kMax` is the maximum i32.
+// `idx` takes `kMax - 4`, `kMax - 2`, and `(kMax - 2) + 2` doesn't overflow.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_LessThan_NotReachRHS_NoOverflow_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(i32(kMax - 4), i32(kMax), 2_i, /* inclusive */ false);
+    ExpectSigned(idx, kMax - 4, kMax - 2);
+}
+
+// for (var idx = kMax - 5; idx < kMax; idx += 2), where `kMax` is the maximum i32.
+// `idx` takes `kMax - 5`, `kMax - 3`, `kMax - 1`, and `(kMax - 1) + 2` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_LessThan_Overflow_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(i32(kMax - 5), i32(kMax), 2_i, /* inclusive */ false);
+    ExpectNoRange(idx);
+}
+
+// for (var idx = 0; idx <= 10; idx += 2147483647)
+// `idx` only takes 0, and `0 + 2147483647` is exactly the maximum i32.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_StepIsHighestValue_i32) {
+    Var* idx = BuildIncreasingLoop(0_i, 10_i, 2147483647_i);
+    ExpectSigned(idx, 0, 0);
+}
+
+// for (var idx = 1; idx <= 10; idx += 2147483647)
+// `1 + 2147483647` overflows i32 (but would not overflow u32).
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_StepIsHighestValue_i32) {
+    Var* idx = BuildIncreasingLoop(1_i, 10_i, 2147483647_i);
+    ExpectNoRange(idx);
+}
+
+// for (var idx = kMax - 7u; idx <= kMax - 2u; idx += 2u), where `kMax` is the maximum u32.
+// `idx` takes `kMax - 7u`, `kMax - 5u`, `kMax - 3u`, and `(kMax - 3u) + 2u` doesn't overflow.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_NotReachRHS_NoOverflow_u32) {
+    constexpr uint32_t kMax = u32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(u32(kMax - 7u), u32(kMax - 2u), 2_u);
+    ExpectUnsigned(idx, kMax - 7u, kMax - 3u);
+}
+
+// for (var idx = kMax - 7u; idx <= kMax - 1u; idx += 2u), where `kMax` is the maximum u32.
+// `idx` takes `kMax - 7u`, `kMax - 5u`, `kMax - 3u`, `kMax - 1u`, and `(kMax - 1u) + 2u`
+// overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_Overflow_u32) {
+    constexpr uint32_t kMax = u32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(u32(kMax - 7u), u32(kMax - 1u), 2_u);
+    ExpectNoRange(idx);
+}
+
+// for (var idx = kMax - 6u; idx < kMax; idx += 3u), where `kMax` is the maximum u32.
+// `idx` takes `kMax - 6u`, `kMax - 3u`, and `(kMax - 3u) + 3u` doesn't overflow.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_LessThan_NotReachRHS_NoOverflow_u32) {
+    constexpr uint32_t kMax = u32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(u32(kMax - 6u), u32(kMax), 3_u, /* inclusive */ false);
+    ExpectUnsigned(idx, kMax - 6u, kMax - 3u);
+}
+
+// for (var idx = kMax - 4u; idx < kMax; idx += 3u), where `kMax` is the maximum u32.
+// `idx` takes `kMax - 4u`, `kMax - 1u`, and `(kMax - 1u) + 3u` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_LessThan_Overflow_u32) {
+    constexpr uint32_t kMax = u32::kHighestValue;
+    Var* idx = BuildIncreasingLoop(u32(kMax - 4u), u32(kMax), 3_u, /* inclusive */ false);
+    ExpectNoRange(idx);
+}
+
+// for (var idx = 0u; idx <= 10u; idx += 4294967295u)
+// `idx` only takes 0u, and `0u + 4294967295u` is exactly the maximum u32.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_StepIsHighestValue_u32) {
+    Var* idx = BuildIncreasingLoop(0_u, 10_u, 4294967295_u);
+    ExpectUnsigned(idx, 0u, 0u);
+}
+
+// for (var idx = 1u; idx <= 10u; idx += 4294967295u)
+// `1u + 4294967295u` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, ConstantInit_Failure_StepIsHighestValue_u32) {
+    Var* idx = BuildIncreasingLoop(1_u, 10_u, 4294967295_u);
+    ExpectNoRange(idx);
+}
+
+// for (var i = 1; i <= 7; i += 1) {         // Range of `i` is [1, 7]
+//   for (var j = i; j < 17; j += 6) {}      // Range of `j` is [1, 16]
+// }
+// `(16 - 1) % 6 = 3` and `1 + 3 = 4` is in [1, 7], so `j` reaches 16 when `i == 4`.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_InnerReachRHS_i32) {
+    auto [i, j] = BuildNestedLoops(1_i, 7_i, 1_i, 17_i, 6_i);
+    ExpectSigned(i, 1, 7);
+    ExpectSigned(j, 1, 16);
+}
+
+// for (var i = 1; i <= 2; i += 1) {         // Range of `i` is [1, 2]
+//   for (var j = i; j < 17; j += 6) {}      // Range of `j` is [1, 14]
+// }
+// `(16 - 1) % 6 = 3` and `1 + 3 = 4` is not in [1, 2], so the maximum value of `j` is reached
+// when `i == 2` (`2 + 6 * 2 = 14`).
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_InnerNotReachRHS_i32) {
+    auto [i, j] = BuildNestedLoops(1_i, 2_i, 1_i, 17_i, 6_i);
+    ExpectSigned(i, 1, 2);
+    ExpectSigned(j, 1, 14);
+}
+
+// for (var i = 1u; i <= 3u; i += 1u) {      // Range of `i` is [1u, 3u]
+//   for (var j = i; j < 17u; j += 6u) {}    // Range of `j` is [1u, 15u]
+// }
+// `(16 - 1) % 6 = 3` and `1 + 3 = 4` is not in [1, 3], so the maximum value of `j` is reached
+// when `i == 3` (`3 + 6 * 2 = 15`).
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_InnerNotReachRHS_Boundary_u32) {
+    auto [i, j] = BuildNestedLoops(1_u, 3_u, 1_u, 17_u, 6_u);
+    ExpectUnsigned(i, 1u, 3u);
+    ExpectUnsigned(j, 1u, 15u);
+}
+
+// for (var i = 1u; i <= 4u; i += 1u) {      // Range of `i` is [1u, 4u]
+//   for (var j = i; j < 17u; j += 6u) {}    // Range of `j` is [1u, 16u]
+// }
+// `(16 - 1) % 6 = 3` and `1 + 3 = 4` is exactly the maximum value of `i`.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_InnerReachRHS_Boundary_u32) {
+    auto [i, j] = BuildNestedLoops(1_u, 4_u, 1_u, 17_u, 6_u);
+    ExpectUnsigned(i, 1u, 4u);
+    ExpectUnsigned(j, 1u, 16u);
+}
+
+// for (var i = 0; i <= 20; i += 5) {        // Range of `i` is [0, 20]
+//   for (var j = i; j < 10; j += 4) {}      // Range of `j` is [0, 9]
+// }
+// The maximum value of `i` is greater than the RHS of the inner loop.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_InitMaxGreaterThanRHS_i32) {
+    auto [i, j] = BuildNestedLoops(0_i, 20_i, 5_i, 10_i, 4_i);
+    ExpectSigned(i, 0, 20);
+    ExpectSigned(j, 0, 9);
+}
+
+// for (var i = 0u; i <= 10u; i += 3u) {     // Range of `i` is [0u, 9u]
+//   for (var j = i; j < 8u; j += 5u) {}     // Range of `j` is [0u, 7u]
+// }
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_BothStepsGreaterThanOne_u32) {
+    auto [i, j] = BuildNestedLoops(0_u, 10_u, 3_u, 8_u, 5_u);
+    ExpectUnsigned(i, 0u, 9u);
+    ExpectUnsigned(j, 0u, 7u);
+}
+
+// for (var i = 10; i <= 20; i += 1) {       // Range of `i` is [10, 20]
+//   for (var j = i; j < 5; j += 2) {}       // The inner loop body is never executed.
+// }
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_Failure_InitMinGreaterThanRHS_i32) {
+    auto [i, j] = BuildNestedLoops(10_i, 20_i, 1_i, 5_i, 2_i);
+    ExpectSigned(i, 10, 20);
+    ExpectNoRange(j);
+}
+
+// for (var i = kMax - 10; i <= kMax - 8; i += 1) {  // Range of `i` is [kMax - 10, kMax - 8]
+//   for (var j = i; j < kMax; j += 4) {}
+// }
+// where `kMax` is the maximum i32.
+// `j` can be `kMax - 1` (when `i == kMax - 9`), and `(kMax - 1) + 4` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_Failure_Overflow_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    auto [i, j] = BuildNestedLoops(i32(kMax - 10), i32(kMax - 8), 1_i, i32(kMax), 4_i);
+    ExpectSigned(i, kMax - 10, kMax - 8);
+    ExpectNoRange(j);
+}
+
+// for (var i = kMax - 11; i <= kMax - 9; i += 1) {  // Range of `i` is [kMax - 11, kMax - 9]
+//   for (var j = i; j < kMax - 2; j += 4) {}
+// }
+// where `kMax` is the maximum i32.
+// `((kMax - 3) - (kMax - 11)) % 4 = 0`, so `j` can be `kMax - 3` (when `i == kMax - 11`), and
+// `(kMax - 3) + 4` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_Failure_Overflow_ReachRHS_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    auto [i, j] = BuildNestedLoops(i32(kMax - 11), i32(kMax - 9), 1_i, i32(kMax - 2), 4_i);
+    ExpectSigned(i, kMax - 11, kMax - 9);
+    ExpectNoRange(j);
+}
+
+// for (var i = kMax - 10; i <= kMax - 8; i += 1) {  // Range of `i` is [kMax - 10, kMax - 8]
+//   for (var j = i; j < kMax - 2; j += 4) {}        // Range of `j` is [kMax - 10, kMax - 4]
+// }
+// where `kMax` is the maximum i32.
+// `((kMax - 3) - (kMax - 10)) % 4 = 3` and `(kMax - 10) + 3 = kMax - 7` is not in the range of
+// `i`, so the maximum value of `j` is reached when `i == kMax - 8` (`kMax - 8 + 4 = kMax - 4`),
+// and `(kMax - 4) + 4` doesn't overflow.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_NotReachRHS_NoOverflow_i32) {
+    constexpr int32_t kMax = i32::kHighestValue;
+    auto [i, j] = BuildNestedLoops(i32(kMax - 10), i32(kMax - 8), 1_i, i32(kMax - 2), 4_i);
+    ExpectSigned(i, kMax - 10, kMax - 8);
+    ExpectSigned(j, kMax - 10, kMax - 4);
+}
+
+// for (var i = kMax - 10u; i <= kMax - 8u; i += 1u) {  // Range of `i` is [kMax - 10u, kMax - 8u]
+//   for (var j = i; j < kMax; j += 3u) {}
+// }
+// where `kMax` is the maximum u32.
+// `j` can be `kMax - 1u` (when `i == kMax - 10u`), and `(kMax - 1u) + 3u` overflows.
+TEST_F(IR_IntegerRangeAnalysisLoopStepTest, NestedLoops_Failure_Overflow_u32) {
+    constexpr uint32_t kMax = u32::kHighestValue;
+    auto [i, j] = BuildNestedLoops(u32(kMax - 10u), u32(kMax - 8u), 1_u, u32(kMax), 3_u);
+    ExpectUnsigned(i, kMax - 10u, kMax - 8u);
+    ExpectNoRange(j);
+}
+
 }  // namespace
 }  // namespace tint::core::ir::analysis