Add spvtools::utils::IndexRange (#6106)

It's a span, but without the base pointer.

Use the 'apply' method on a base pointer to generate a corresponding span.
diff --git a/source/util/index_range.h b/source/util/index_range.h
new file mode 100644
index 0000000..ee2299f
--- /dev/null
+++ b/source/util/index_range.h
@@ -0,0 +1,72 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_UTIL_INDEX_RANGE_H_
+#define SOURCE_UTIL_INDEX_RANGE_H_
+
+#include <cassert>
+#include <cstdint>
+
+#include "source/util/span.h"
+
+namespace spvtools {
+namespace utils {
+
+// Implement a range of indicies, to index over an array of values of type T,
+// but whose base pointer is supplied externally.  Think of this as a span
+// but without the base pointer, which is to be applied later.  Parameterization
+// by T makes usage more readable and less error-prone.
+template <typename T, class IndexType = uint32_t, class CountType = IndexType>
+class IndexRange {
+ public:
+  static_assert(std::is_integral<IndexType>::value);
+  static_assert(std::is_unsigned<IndexType>::value);
+  static_assert(std::is_integral<CountType>::value);
+  static_assert(std::is_unsigned<CountType>::value);
+  using value_type = T;
+  using index_type = IndexType;
+  using size_type = CountType;
+
+  constexpr IndexRange() {}
+  constexpr IndexRange(index_type first, size_type count)
+      : first_(first), count_(count) {}
+
+  size_type count() const { return count_; }
+  bool empty() const { return count() == size_type(0); }
+
+  IndexType first() const { return first_; }
+
+  // Returns the span of indexed elements using the given base pointer.
+  template <typename E>
+  spvtools::utils::Span<E> apply(E* base) const {
+    using span_type = spvtools::utils::Span<E>;
+    return base ? span_type(base + first_, count_) : span_type();
+  }
+  // This specialization lets us pass in a null pointer, which yields
+  // an empty span.
+  template <typename E = int>
+  spvtools::utils::Span<E> apply(std::nullptr_t) const {
+    using span_type = spvtools::utils::Span<E>;
+    return span_type();
+  }
+
+ private:
+  index_type first_ = 0;
+  size_type count_ = 0;
+};
+
+}  // namespace utils
+}  // namespace spvtools
+
+#endif  // SOURCE_UTIL_INDEX_RANGE_H_
diff --git a/test/util/CMakeLists.txt b/test/util/CMakeLists.txt
index 33bb931..c5cbc17 100644
--- a/test/util/CMakeLists.txt
+++ b/test/util/CMakeLists.txt
@@ -17,6 +17,7 @@
        bit_vector_test.cpp
        bitutils_test.cpp
        hash_combine_test.cpp
+       index_range_test.cpp
        small_vector_test.cpp
        span_test.cpp
   LIBS SPIRV-Tools-opt
diff --git a/test/util/index_range_test.cpp b/test/util/index_range_test.cpp
new file mode 100644
index 0000000..0ae3954
--- /dev/null
+++ b/test/util/index_range_test.cpp
@@ -0,0 +1,79 @@
+// Copyright (c) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/util/index_range.h"
+
+#include "gmock/gmock.h"
+
+namespace spvtools {
+namespace utils {
+namespace {
+
+using IndexRangeTest = ::testing::Test;
+
+using ushort = unsigned short;
+
+TEST(IndexRangeTest, Initialize_Default) {
+  double sentinel_a = 0.0;
+  double sentinel_b = 1.0;
+  const IndexRange<double, unsigned, ushort> ir;
+
+  EXPECT_EQ(ir.first(), unsigned(0));
+  EXPECT_EQ(ir.count(), ushort(0));
+  EXPECT_TRUE(ir.empty());
+
+  auto span_null = ir.apply(nullptr);
+  EXPECT_EQ(span_null.data(), nullptr);
+  EXPECT_EQ(span_null.size(), 0);
+  EXPECT_TRUE(span_null.empty());
+
+  auto span_a = ir.apply(&sentinel_a);
+  EXPECT_EQ(span_a.data(), &sentinel_a);
+  EXPECT_EQ(span_a.size(), 0);
+  EXPECT_TRUE(span_a.empty());
+
+  auto span_b = ir.apply(&sentinel_b);
+  EXPECT_EQ(span_b.data(), &sentinel_b);
+  EXPECT_EQ(span_b.size(), 0);
+  EXPECT_TRUE(span_b.empty());
+}
+
+TEST(IndexRangeTest, Initialize_NonEmpty) {
+  const IndexRange<double, unsigned, ushort> ir(1, 2);
+
+  EXPECT_EQ(ir.first(), unsigned(1));
+  EXPECT_EQ(ir.count(), ushort(2));
+  EXPECT_FALSE(ir.empty());
+
+  auto span_null = ir.apply(nullptr);
+  EXPECT_EQ(span_null.data(), nullptr);
+  EXPECT_EQ(span_null.size(), 0);
+  EXPECT_TRUE(span_null.empty());
+
+  double arr[] = {0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0};
+
+  auto span_a = ir.apply(arr);
+  EXPECT_EQ(span_a.begin(), arr + 1);
+  EXPECT_EQ(span_a.end(), arr + 3);
+  EXPECT_FALSE(span_a.empty());
+
+  auto span_b = ir.apply(arr + 3);
+  EXPECT_EQ(span_b.begin(), arr + 4);
+  EXPECT_EQ(span_b.end(), arr + 6);
+  EXPECT_FALSE(span_a.empty());
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace spvtools