Range Analysis: Compute range of built-in call `max`

This patch computes the range of a core built-in call `max`.
- When both `a` and `b` are constant values, the range of
  `max(a, b)` will also be a constant value.
- When the range of `a` is [min1, max1] and the range of `b` is
  [min2, max2], the range of `max(a, b)` is `[max(min1, min2),
  max(max1, max2)]`.
- The ranges that represent the whole range of `i32` or `u32`
  are treated as invalid ones (e.g. max(a, i32::kLowestValue)).

Bug: 348701956
Test: tint_unittests
Change-Id: Id2c650b289da7221744c28ca92ca92d8c0a84b6b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/253576
Commit-Queue: Jiawei Shao <jiawei.shao@intel.com>
Reviewed-by: James Price <jrprice@google.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 533bc4d..cdd28a9 100644
--- a/src/tint/lang/core/ir/analysis/integer_range_analysis.cc
+++ b/src/tint/lang/core/ir/analysis/integer_range_analysis.cc
@@ -108,11 +108,6 @@
     }
 }
 
-template <typename IntegerRange>
-bool IntegerRangeIsConstantValue(const IntegerRange& range) {
-    return range.min_bound == range.max_bound;
-}
-
 }  // namespace
 
 IntegerRangeInfo::IntegerRangeInfo(int64_t min_bound, int64_t max_bound) {
@@ -310,6 +305,9 @@
                 case core::BuiltinFn::kMin: {
                     return ComputeIntegerRangeForBuiltinMin(call);
                 }
+                case core::BuiltinFn::kMax: {
+                    return ComputeIntegerRangeForBuiltinMax(call);
+                }
                 default:
                     return {};
             }
@@ -1153,13 +1151,6 @@
             auto range1_i32 = std::get<IntegerRangeInfo::SignedIntegerRange>(range1.range);
             auto range2_i32 = std::get<IntegerRangeInfo::SignedIntegerRange>(range2.range);
 
-            // When both operands are constant values, we can return the constant value.
-            if (IntegerRangeIsConstantValue(range1_i32) &&
-                IntegerRangeIsConstantValue(range2_i32)) {
-                int64_t constant_value = std::min(range1_i32.min_bound, range2_i32.min_bound);
-                return IntegerRangeInfo(constant_value, constant_value);
-            }
-
             // When the range is all i32 or u32, we will treat it as an invalid range.
             int64_t min_bound = std::min(range1_i32.min_bound, range2_i32.min_bound);
             int64_t max_bound = std::min(range1_i32.max_bound, range2_i32.max_bound);
@@ -1175,13 +1166,6 @@
             auto range1_u32 = std::get<IntegerRangeInfo::UnsignedIntegerRange>(range1.range);
             auto range2_u32 = std::get<IntegerRangeInfo::UnsignedIntegerRange>(range2.range);
 
-            // When both operands are constant values, we can return the constant value.
-            if (IntegerRangeIsConstantValue(range1_u32) &&
-                IntegerRangeIsConstantValue(range2_u32)) {
-                uint64_t constant_value = std::min(range1_u32.min_bound, range2_u32.min_bound);
-                return IntegerRangeInfo(constant_value, constant_value);
-            }
-
             // When the range is all i32 or u32, we will treat it as an invalid range.
             uint64_t min_bound = std::min(range1_u32.min_bound, range2_u32.min_bound);
             uint64_t max_bound = std::min(range1_u32.max_bound, range2_u32.max_bound);
@@ -1193,6 +1177,60 @@
         }
     }
 
+    IntegerRangeInfo ComputeIntegerRangeForBuiltinMax(const CoreBuiltinCall* call) {
+        TINT_ASSERT(call->Operands().Length() == 2u);
+
+        TINT_ASSERT(call->Operand(0)->Type()->IsIntegerScalar());
+        TINT_ASSERT(call->Operand(1)->Type()->IsIntegerScalar());
+
+        IntegerRangeInfo range1 = GetInfo(call->Operand(0));
+        IntegerRangeInfo range2 = GetInfo(call->Operand(1));
+        if (!range1.IsValid() && !range2.IsValid()) {
+            return {};
+        }
+
+        if (!range1.IsValid()) {
+            range1 = GetFullRangeWithSameIntegerRangeInfoType(range2);
+        }
+        if (!range2.IsValid()) {
+            range2 = GetFullRangeWithSameIntegerRangeInfoType(range1);
+        }
+
+        // range1: [min1, max1]  range2: [min2, max2]
+        // The minimum value of (max(range1, range2)) is max(min1, min2) and
+        // The maximum value of (max(range1, range2)) is max(max1, max2).
+        if (std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(range1.range)) {
+            TINT_ASSERT(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(range2.range));
+
+            auto range1_i32 = std::get<IntegerRangeInfo::SignedIntegerRange>(range1.range);
+            auto range2_i32 = std::get<IntegerRangeInfo::SignedIntegerRange>(range2.range);
+
+            // When the range is all i32 or u32, we will treat it as an invalid range.
+            int64_t min_bound = std::max(range1_i32.min_bound, range2_i32.min_bound);
+            int64_t max_bound = std::max(range1_i32.max_bound, range2_i32.max_bound);
+            if (min_bound == i32::kLowestValue && max_bound == i32::kHighestValue) {
+                return {};
+            }
+
+            return IntegerRangeInfo(min_bound, max_bound);
+        } else {
+            TINT_ASSERT(
+                std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(range2.range));
+
+            auto range1_u32 = std::get<IntegerRangeInfo::UnsignedIntegerRange>(range1.range);
+            auto range2_u32 = std::get<IntegerRangeInfo::UnsignedIntegerRange>(range2.range);
+
+            // When the range is all i32 or u32, we will treat it as an invalid range.
+            uint64_t min_bound = std::max(range1_u32.min_bound, range2_u32.min_bound);
+            uint64_t max_bound = std::max(range1_u32.max_bound, range2_u32.max_bound);
+            if (min_bound == u32::kLowestValue && max_bound == u32::kHighestValue) {
+                return {};
+            }
+
+            return IntegerRangeInfo(min_bound, max_bound);
+        }
+    }
+
     Hashmap<const FunctionParam*, Vector<IntegerRangeInfo, 3>, 4>
         integer_function_param_range_info_map_;
     Hashmap<const Var*, IntegerRangeInfo, 8> integer_var_range_info_map_;
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 6b5718b..81ed7e4 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
@@ -15719,5 +15719,1370 @@
     EXPECT_EQ(19u, range.max_bound);
 }
 
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_F32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().f32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        call_max = b.Call<f32>(BuiltinFn::kMax, param, 1.0_f);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:f32):void {
+  $B1: {
+    %3:f32 = max %param, 1.0f
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, 1.0f)`)
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_Vector_I32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().vec4<i32>());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* vec4_const = b.Construct(ty.vec4<i32>(), 1_i, 2_i, 3_i, 4_i);
+        call_max = b.Call<vec4<i32>>(BuiltinFn::kMax, param, vec4_const);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:vec4<i32>):void {
+  $B1: {
+    %3:vec4<i32> = construct 1i, 2i, 3i, 4i
+    %4:vec4<i32> = max %param, %3
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, vec4i(1, 2, 3, 4))`)
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_Vector_U32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().vec2<u32>());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* vec2_const = b.Construct(ty.vec2<u32>(), 1_u, 2_u);
+        call_max = b.Call<vec2<u32>>(BuiltinFn::kMax, param, vec2_const);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:vec2<u32>):void {
+  $B1: {
+    %3:vec2<u32> = construct 1u, 2u
+    %4:vec2<u32> = max %param, %3
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, vec2u(1, 2))`)
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_BothInvalidRange_I32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param1 = b.FunctionParam("param1", mod.Types().i32());
+    auto* param2 = b.FunctionParam("param2", mod.Types().i32());
+    func->AppendParam(param1);
+    func->AppendParam(param2);
+
+    b.Append(func->Block(), [&] {
+        call_max = b.Call<i32>(BuiltinFn::kMax, param1, param2);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param1:i32, %param2:i32):void {
+  $B1: {
+    %4:i32 = max %param1, %param2
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param1, param2)`)
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_BothInvalidRange_U32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param1 = b.FunctionParam("param1", mod.Types().u32());
+    auto* param2 = b.FunctionParam("param2", mod.Types().u32());
+    func->AppendParam(param1);
+    func->AppendParam(param2);
+
+    b.Append(func->Block(), [&] {
+        call_max = b.Call<u32>(BuiltinFn::kMax, param1, param2);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param1:u32, %param2:u32):void {
+  $B1: {
+    %4:u32 = max %param1, %param2
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param1, param2)`)
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_BothAreConstantValues_I32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = -4
+            idx = b.Var("idx", -4_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < -3
+            auto* binary = b.LessThan<bool>(b.Load(idx), -3_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            Var* idy = nullptr;
+            auto* loop2 = b.Loop();
+            b.Append(loop2->Initializer(), [&] {
+                // idy = 2
+                idy = b.Var("idy", 2_i);
+                b.NextIteration(loop2);
+            });
+            b.Append(loop2->Body(), [&] {
+                // idy < 3
+                auto* binary_inner = b.LessThan<bool>(b.Load(idy), 3_i);
+                auto* ifelse_inner = b.If(binary_inner);
+                b.Append(ifelse_inner->True(), [&] { b.ExitIf(ifelse_inner); });
+                b.Append(ifelse_inner->False(), [&] { b.ExitLoop(loop2); });
+                auto* loadx = b.Load(idx);
+                auto* loady = b.Load(idy);
+                // call_max = max(idx, idy);
+                call_max = b.Call<i32>(BuiltinFn::kMax, loadx, loady);
+                b.Continue(loop2);
+            });
+            b.Append(loop2->Continuing(), [&] {
+                // idy++
+                b.Store(idy, b.Add<i32>(b.Load(idy), 1_i));
+                b.NextIteration(loop2);
+            });
+
+            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* src = R"(
+%func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var -4i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %3:i32 = load %idx
+        %4:bool = lt %3, -3i
+        if %4 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        loop [i: $B7, b: $B8, c: $B9] {  # loop_2
+          $B7: {  # initializer
+            %idy:ptr<function, i32, read_write> = var 2i
+            next_iteration  # -> $B8
+          }
+          $B8: {  # body
+            %6:i32 = load %idy
+            %7:bool = lt %6, 3i
+            if %7 [t: $B10, f: $B11] {  # if_2
+              $B10: {  # true
+                exit_if  # if_2
+              }
+              $B11: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            %8:i32 = load %idx
+            %9:i32 = load %idy
+            %10:i32 = max %8, %9
+            continue  # -> $B9
+          }
+          $B9: {  # continuing
+            %11:i32 = load %idy
+            %12:i32 = add %11, 1i
+            store %idy, %12
+            next_iteration  # -> $B8
+          }
+        }
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %13:i32 = load %idx
+        %14:i32 = add %13, 1i
+        store %idx, %14
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(idx, idy)`)
+    // idx: [-4, -4] idy: [2, 2]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::SignedIntegerRange>(info.range);
+    EXPECT_EQ(2, range.min_bound);
+    EXPECT_EQ(2, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_BothAreConstantValues_U32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 5u
+            idx = b.Var("idx", 5_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 6u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 6_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            Var* idy = nullptr;
+            auto* loop2 = b.Loop();
+            b.Append(loop2->Initializer(), [&] {
+                // idy = 1u
+                idy = b.Var("idy", 1_u);
+                b.NextIteration(loop2);
+            });
+            b.Append(loop2->Body(), [&] {
+                // idy < 2u
+                auto* binary_inner = b.LessThan<bool>(b.Load(idy), 2_u);
+                auto* ifelse_inner = b.If(binary_inner);
+                b.Append(ifelse_inner->True(), [&] { b.ExitIf(ifelse_inner); });
+                b.Append(ifelse_inner->False(), [&] { b.ExitLoop(loop2); });
+                auto* loadx = b.Load(idx);
+                auto* loady = b.Load(idy);
+                // call_max = max(idx, idy);
+                call_max = b.Call<u32>(BuiltinFn::kMax, loadx, loady);
+                b.Continue(loop2);
+            });
+            b.Append(loop2->Continuing(), [&] {
+                // idy++
+                b.Store(idy, b.Add<u32>(b.Load(idy), 1_u));
+                b.NextIteration(loop2);
+            });
+
+            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* src = R"(
+%func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 5u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %3:u32 = load %idx
+        %4:bool = lt %3, 6u
+        if %4 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        loop [i: $B7, b: $B8, c: $B9] {  # loop_2
+          $B7: {  # initializer
+            %idy:ptr<function, u32, read_write> = var 1u
+            next_iteration  # -> $B8
+          }
+          $B8: {  # body
+            %6:u32 = load %idy
+            %7:bool = lt %6, 2u
+            if %7 [t: $B10, f: $B11] {  # if_2
+              $B10: {  # true
+                exit_if  # if_2
+              }
+              $B11: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            %8:u32 = load %idx
+            %9:u32 = load %idy
+            %10:u32 = max %8, %9
+            continue  # -> $B9
+          }
+          $B9: {  # continuing
+            %11:u32 = load %idy
+            %12:u32 = add %11, 1u
+            store %idy, %12
+            next_iteration  # -> $B8
+          }
+        }
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %13:u32 = load %idx
+        %14:u32 = add %13, 1u
+        store %idx, %14
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(idx, idy)`)
+    // idx: [5u, 5u] idy: [1u, 1u]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::UnsignedIntegerRange>(info.range);
+    EXPECT_EQ(5u, range.min_bound);
+    EXPECT_EQ(5u, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_BothValidRange_I32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = -4
+            idx = b.Var("idx", -4_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 9
+            auto* binary = b.LessThan<bool>(b.Load(idx), 9_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            Var* idy = nullptr;
+            auto* loop2 = b.Loop();
+            b.Append(loop2->Initializer(), [&] {
+                // idy = 4
+                idy = b.Var("idy", 4_i);
+                b.NextIteration(loop2);
+            });
+            b.Append(loop2->Body(), [&] {
+                // idy < 6
+                auto* binary_inner = b.LessThan<bool>(b.Load(idy), 6_i);
+                auto* ifelse_inner = b.If(binary_inner);
+                b.Append(ifelse_inner->True(), [&] { b.ExitIf(ifelse_inner); });
+                b.Append(ifelse_inner->False(), [&] { b.ExitLoop(loop2); });
+                auto* loadx = b.Load(idx);
+                auto* loady = b.Load(idy);
+                // call_max = max(idx, idy);
+                call_max = b.Call<i32>(BuiltinFn::kMax, loadx, loady);
+                b.Continue(loop2);
+            });
+            b.Append(loop2->Continuing(), [&] {
+                // idy++
+                b.Store(idy, b.Add<i32>(b.Load(idy), 1_i));
+                b.NextIteration(loop2);
+            });
+
+            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* src = R"(
+%func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var -4i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %3:i32 = load %idx
+        %4:bool = lt %3, 9i
+        if %4 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        loop [i: $B7, b: $B8, c: $B9] {  # loop_2
+          $B7: {  # initializer
+            %idy:ptr<function, i32, read_write> = var 4i
+            next_iteration  # -> $B8
+          }
+          $B8: {  # body
+            %6:i32 = load %idy
+            %7:bool = lt %6, 6i
+            if %7 [t: $B10, f: $B11] {  # if_2
+              $B10: {  # true
+                exit_if  # if_2
+              }
+              $B11: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            %8:i32 = load %idx
+            %9:i32 = load %idy
+            %10:i32 = max %8, %9
+            continue  # -> $B9
+          }
+          $B9: {  # continuing
+            %11:i32 = load %idy
+            %12:i32 = add %11, 1i
+            store %idy, %12
+            next_iteration  # -> $B8
+          }
+        }
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %13:i32 = load %idx
+        %14:i32 = add %13, 1i
+        store %idx, %14
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(idx, idy)`)
+    // idx: [-4, 8] idy: [4, 5]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::SignedIntegerRange>(info.range);
+    EXPECT_EQ(4, range.min_bound);
+    EXPECT_EQ(8, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_BothValidRange_U32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 4u
+            idx = b.Var("idx", 4_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 9u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 9_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            Var* idy = nullptr;
+            auto* loop2 = b.Loop();
+            b.Append(loop2->Initializer(), [&] {
+                // idy = 1u
+                idy = b.Var("idy", 1_u);
+                b.NextIteration(loop2);
+            });
+            b.Append(loop2->Body(), [&] {
+                // idy < 8u
+                auto* binary_inner = b.LessThan<bool>(b.Load(idy), 8_u);
+                auto* ifelse_inner = b.If(binary_inner);
+                b.Append(ifelse_inner->True(), [&] { b.ExitIf(ifelse_inner); });
+                b.Append(ifelse_inner->False(), [&] { b.ExitLoop(loop2); });
+                auto* loadx = b.Load(idx);
+                auto* loady = b.Load(idy);
+                // call_max = max(idx, idy);
+                call_max = b.Call<u32>(BuiltinFn::kMax, loadx, loady);
+                b.Continue(loop2);
+            });
+            b.Append(loop2->Continuing(), [&] {
+                // idy++
+                b.Store(idy, b.Add<u32>(b.Load(idy), 1_u));
+                b.NextIteration(loop2);
+            });
+
+            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* src = R"(
+%func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 4u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %3:u32 = load %idx
+        %4:bool = lt %3, 9u
+        if %4 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_if  # if_1
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        loop [i: $B7, b: $B8, c: $B9] {  # loop_2
+          $B7: {  # initializer
+            %idy:ptr<function, u32, read_write> = var 1u
+            next_iteration  # -> $B8
+          }
+          $B8: {  # body
+            %6:u32 = load %idy
+            %7:bool = lt %6, 8u
+            if %7 [t: $B10, f: $B11] {  # if_2
+              $B10: {  # true
+                exit_if  # if_2
+              }
+              $B11: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            %8:u32 = load %idx
+            %9:u32 = load %idy
+            %10:u32 = max %8, %9
+            continue  # -> $B9
+          }
+          $B9: {  # continuing
+            %11:u32 = load %idy
+            %12:u32 = add %11, 1u
+            store %idy, %12
+            next_iteration  # -> $B8
+          }
+        }
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %13:u32 = load %idx
+        %14:u32 = add %13, 1u
+        store %idx, %14
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(idx, idy)`)
+    // idx: [4u, 8u] idy: [1u, 7u]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::UnsignedIntegerRange>(info.range);
+    EXPECT_EQ(4u, range.min_bound);
+    EXPECT_EQ(8u, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_FirstIsInvalidRange_InvalidResult_I32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().i32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* min_i32 = b.Constant(i32::Lowest());
+        call_max = b.Call<i32>(BuiltinFn::kMax, param, min_i32);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    %3:i32 = max %param, -2147483648i
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, min_i32)`)
+    // min_i32 = i32::kLowestValue, param: [i32::kLowestValue, i32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_FirstIsInvalidRange_InvalidResult_U32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().u32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* min_u32 = b.Constant(u32::Lowest());
+        call_max = b.Call<u32>(BuiltinFn::kMax, param, min_u32);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    %3:u32 = max %param, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, min_u32)`)
+    // min_u32 = u32::kLowestValue, param: [u32::kLowestValue, u32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_SecondIsInvalidRange_InvalidResult_I32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().i32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* min_i32 = b.Constant(i32::Lowest());
+        call_max = b.Call<i32>(BuiltinFn::kMax, min_i32, param);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    %3:i32 = max -2147483648i, %param
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(min_i32, param)`)
+    // min_i32 = i32::kLowestValue, param: [i32::kLowestValue, i32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Failure_SecondIsInvalidRange_InvalidResult_U32) {
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().u32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* min_u32 = b.Constant(u32::Lowest());
+        call_max = b.Call<u32>(BuiltinFn::kMax, min_u32, param);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    %3:u32 = max 0u, %param
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(min_u32, param)`)
+    // min_u32 = u32::kHighestValue, param: [u32::kLowestValue, u32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_FALSE(info.IsValid());
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_FirstIsInvalidRange_I32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().i32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 1
+            idx = b.Var("idx", 1_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 10
+            auto* binary = b.LessThan<bool>(b.Load(idx), 10_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            // call_max = max(param, idx);
+            auto* loadx = b.Load(idx);
+            call_max = b.Call<i32>(BuiltinFn::kMax, param, loadx);
+
+            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* src = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    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, 10i
+        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:i32 = max %param, %6
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %8:i32 = load %idx
+        %9:i32 = add %8, 1i
+        store %idx, %9
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, idx)`)
+    // idx: [1, 9], param: [i32::kLowestValue, i32::kHighestValue]
+    // call_max: [1, i32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::SignedIntegerRange>(info.range);
+    EXPECT_EQ(1, range.min_bound);
+    EXPECT_EQ(i32::kHighestValue, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_FirstIsInvalidRange_U32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().u32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 1u
+            idx = b.Var("idx", 1_u);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < 10u
+            auto* binary = b.LessThan<bool>(b.Load(idx), 10_u);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            // call_max = max(param, idx);
+            auto* loadx = b.Load(idx);
+            call_max = b.Call<u32>(BuiltinFn::kMax, param, loadx);
+
+            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* src = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 1u
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:u32 = load %idx
+        %5:bool = lt %4, 10u
+        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 = max %param, %6
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %8:u32 = load %idx
+        %9:u32 = add %8, 1u
+        store %idx, %9
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(param, idx)`)
+    // idx: [1, 9], param: [u32::kLowestValue, u32::kHighestValue]
+    // call_max: [1, u32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::UnsignedIntegerRange>(info.range);
+    EXPECT_EQ(1u, range.min_bound);
+    EXPECT_EQ(u32::kHighestValue, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_SecondIsInvalidRange_I32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().i32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = -10
+            idx = b.Var("idx", -10_i);
+            b.NextIteration(loop);
+        });
+        b.Append(loop->Body(), [&] {
+            // idx < -2
+            auto* binary = b.LessThan<bool>(b.Load(idx), -2_i);
+            auto* ifelse = b.If(binary);
+            b.Append(ifelse->True(), [&] { b.ExitIf(ifelse); });
+            b.Append(ifelse->False(), [&] { b.ExitLoop(loop); });
+
+            // call_max = max(idx, param);
+            auto* loadx = b.Load(idx);
+            call_max = b.Call<i32>(BuiltinFn::kMax, loadx, param);
+
+            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* src = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, i32, read_write> = var -10i
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        %4:i32 = load %idx
+        %5:bool = lt %4, -2i
+        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:i32 = max %6, %param
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %8:i32 = load %idx
+        %9:i32 = add %8, 1i
+        store %idx, %9
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(idx, param)`)
+    // idx: [-10, -3], param: [i32::kLowestValue, i32::kHighestValue]
+    // call_max: [-10, i32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::SignedIntegerRange>(info.range);
+    EXPECT_EQ(-10, range.min_bound);
+    EXPECT_EQ(i32::kHighestValue, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Success_SecondIsInvalidRange_U32) {
+    Var* idx = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().u32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] {
+            // idx = 10u
+            idx = b.Var("idx", 10_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); });
+
+            // call_max = max(idx, param);
+            auto* loadx = b.Load(idx);
+            call_max = b.Call<u32>(BuiltinFn::kMax, loadx, param);
+
+            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* src = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        %idx:ptr<function, u32, read_write> = var 10u
+        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:u32 = max %6, %param
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %8:u32 = load %idx
+        %9:u32 = add %8, 1u
+        store %idx, %9
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(idx, param)`)
+    // idx: [10, 19], param: [u32::kLowestValue, u32::kHighestValue]
+    // call_max: [10, u32::kHighestValue]
+    const auto& info = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info.range));
+    const auto& range = std::get<IntegerRangeInfo::UnsignedIntegerRange>(info.range);
+    EXPECT_EQ(10u, range.min_bound);
+    EXPECT_EQ(u32::kHighestValue, range.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Builtin_Input_Success_I32) {
+    CoreBuiltinCall* call_min = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().i32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* bound1 = b.Constant(-5_i);
+        call_min = b.Call<i32>(BuiltinFn::kMin, bound1, param);
+        auto* bound2 = b.Constant(3_i);
+        call_max = b.Call<i32>(BuiltinFn::kMax, bound2, call_min);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    %3:i32 = min -5i, %param
+    %4:i32 = max 3i, %3
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_min` (`min(bound1, param)`)
+    // bound1: [-5, -5] param: [i32::kLowestValue, i32::kHighestValue]
+    // call_min: [i32::kLowestValue, -5]
+    const auto& info_call_min = analysis.GetInfo(call_min);
+    ASSERT_TRUE(info_call_min.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info_call_min.range));
+    const auto& range_call_min =
+        std::get<IntegerRangeInfo::SignedIntegerRange>(info_call_min.range);
+    EXPECT_EQ(i32::kLowestValue, range_call_min.min_bound);
+    EXPECT_EQ(-5, range_call_min.max_bound);
+
+    // Range of `call_max` (`max(bound2, call_min)`)
+    // bound2: [3, 3] call_min: [i32::kLowestValue, -5]
+    // call_max: [3, 3]
+    const auto& info_call_max = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info_call_max.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info_call_max.range));
+    const auto& range_call_max =
+        std::get<IntegerRangeInfo::SignedIntegerRange>(info_call_max.range);
+    EXPECT_EQ(3, range_call_max.min_bound);
+    EXPECT_EQ(3, range_call_max.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Max_Builtin_Input_Success_U32) {
+    CoreBuiltinCall* call_min = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().u32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* bound1 = b.Constant(5_u);
+        call_min = b.Call<u32>(BuiltinFn::kMin, bound1, param);
+        auto* bound2 = b.Constant(3_u);
+        call_max = b.Call<u32>(BuiltinFn::kMax, bound2, call_min);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    %3:u32 = min 5u, %param
+    %4:u32 = max 3u, %3
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_min` (`min(bound1, param)`)
+    // bound1: [5u, 5u] param: [u32::kLowestValue, u32::kHighestValue]
+    // call_min: [u32::kLowestValue, 5u]
+    const auto& info_call_min = analysis.GetInfo(call_min);
+    ASSERT_TRUE(info_call_min.IsValid());
+    ASSERT_TRUE(
+        std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info_call_min.range));
+    const auto& range_call_min =
+        std::get<IntegerRangeInfo::UnsignedIntegerRange>(info_call_min.range);
+    EXPECT_EQ(u32::kLowestValue, range_call_min.min_bound);
+    EXPECT_EQ(5u, range_call_min.max_bound);
+
+    // Range of `call_max` (`max(bound2, call_min)`)
+    // bound2: [3u, 3u] call_min: [u32::kLowestValue, 5u]
+    // call_max: [3u, 5u]
+    const auto& info_call_max = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info_call_max.IsValid());
+    ASSERT_TRUE(
+        std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info_call_max.range));
+    const auto& range_call_max =
+        std::get<IntegerRangeInfo::UnsignedIntegerRange>(info_call_max.range);
+    EXPECT_EQ(3u, range_call_max.min_bound);
+    EXPECT_EQ(5u, range_call_max.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Min_Builtin_Input_Success_I32) {
+    CoreBuiltinCall* call_min = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().i32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* bound1 = b.Constant(-3_i);
+        call_max = b.Call<i32>(BuiltinFn::kMax, bound1, param);
+        auto* bound2 = b.Constant(5_i);
+        call_min = b.Call<i32>(BuiltinFn::kMin, bound2, call_max);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:i32):void {
+  $B1: {
+    %3:i32 = max -3i, %param
+    %4:i32 = min 5i, %3
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(bound1, param)`)
+    // bound1: [-3, -3] param: [i32::kLowestValue, i32::kHighestValue]
+    // call_max: [-3, i32::kHighestValue]
+    const auto& info_call_max = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info_call_max.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info_call_max.range));
+    const auto& range_call_max =
+        std::get<IntegerRangeInfo::SignedIntegerRange>(info_call_max.range);
+    EXPECT_EQ(-3, range_call_max.min_bound);
+    EXPECT_EQ(i32::kHighestValue, range_call_max.max_bound);
+
+    // Range of `call_min` (`min(bound2, call_max)`)
+    // bound2: [5, 5] call_max: [-3, i32::kHighestValue]
+    // call_min: [-3, 5]
+    const auto& info_call_min = analysis.GetInfo(call_min);
+    ASSERT_TRUE(info_call_min.IsValid());
+    ASSERT_TRUE(std::holds_alternative<IntegerRangeInfo::SignedIntegerRange>(info_call_min.range));
+    const auto& range_call_min =
+        std::get<IntegerRangeInfo::SignedIntegerRange>(info_call_min.range);
+    EXPECT_EQ(-3, range_call_min.min_bound);
+    EXPECT_EQ(5, range_call_min.max_bound);
+}
+
+TEST_F(IR_IntegerRangeAnalysisTest, Builtin_Min_Builtin_Input_Success_U32) {
+    CoreBuiltinCall* call_min = nullptr;
+    CoreBuiltinCall* call_max = nullptr;
+
+    auto* func = b.Function("func", ty.void_());
+    auto* param = b.FunctionParam("param", mod.Types().u32());
+    func->AppendParam(param);
+
+    b.Append(func->Block(), [&] {
+        auto* bound1 = b.Constant(5_u);
+        call_max = b.Call<u32>(BuiltinFn::kMax, bound1, param);
+        auto* bound2 = b.Constant(3_u);
+        call_min = b.Call<u32>(BuiltinFn::kMin, bound2, call_max);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%func = func(%param:u32):void {
+  $B1: {
+    %3:u32 = max 5u, %param
+    %4:u32 = min 3u, %3
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+    EXPECT_EQ(Validate(mod), Success);
+
+    IntegerRangeAnalysis analysis(&mod);
+
+    // Range of `call_max` (`max(bound1, param)`)
+    // bound1: [5u, 5u] param: [u32::kLowestValue, u32::kHighestValue]
+    // call_max: [5, u32::kHighestValue]
+    const auto& info_call_max = analysis.GetInfo(call_max);
+    ASSERT_TRUE(info_call_max.IsValid());
+    ASSERT_TRUE(
+        std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info_call_max.range));
+    const auto& range_call_max =
+        std::get<IntegerRangeInfo::UnsignedIntegerRange>(info_call_max.range);
+    EXPECT_EQ(5u, range_call_max.min_bound);
+    EXPECT_EQ(u32::kHighestValue, range_call_max.max_bound);
+
+    // Range of `call_min` (`min(bound2, call_max)`)
+    // bound2: [3u, 3u] call_max: [5u, u32::kHighestValue]
+    // call_min: [3u, 3u]
+    const auto& info_call_min = analysis.GetInfo(call_min);
+    ASSERT_TRUE(info_call_min.IsValid());
+    ASSERT_TRUE(
+        std::holds_alternative<IntegerRangeInfo::UnsignedIntegerRange>(info_call_min.range));
+    const auto& range_call_min =
+        std::get<IntegerRangeInfo::UnsignedIntegerRange>(info_call_min.range);
+    EXPECT_EQ(3u, range_call_min.min_bound);
+    EXPECT_EQ(3u, range_call_min.max_bound);
+}
+
 }  // namespace
 }  // namespace tint::core::ir::analysis