Add IU64 and Span classes, to compute spans over all integers (#9021)

IU64 is a single numeric representation for both signed and unsigned
integers: it can contain as many negative values as a signed number can,
but also as many unsigned as well (so it needs more than 64 bits).

The Span class is a simple representation of contiguous Spans of
numbers.
diff --git a/src/support/iu64.h b/src/support/iu64.h
new file mode 100644
index 0000000..b916665
--- /dev/null
+++ b/src/support/iu64.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2026 WebAssembly Community Group participants
+ *
+ * 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 wasm_support_i65_h
+#define wasm_support_i65_h
+
+#include <cstdint>
+#include <iostream>
+#include <limits>
+
+namespace wasm {
+
+// An integer capable of representing numbers in the combined range of 32 and
+// 64-bit integers, both signed and unsigned. That is, in the range
+//
+//  std::numeric_limits<int64_t>::min() .. std::numeric_limits<uint64_t>::max()
+//
+// This is basically an i64 combined with a u64 in terms of range, hence "IU64".
+struct IU64 {
+  // A 64-bit payload with an extra 65th sign bit.
+  uint64_t value = 0;
+  bool negative = false;
+
+  constexpr IU64() = default;
+
+  // Unsigned values are simple.
+  constexpr IU64(uint32_t x) : value(x) {}
+  constexpr IU64(uint64_t x) : value(x) {}
+
+  // Signed values need to be checked for being negative.
+  constexpr IU64(int32_t x) {
+    if (x >= 0) {
+      value = x;
+    } else {
+      negative = true;
+      value = -int64_t(x);
+    }
+  }
+  constexpr IU64(int64_t x) {
+    if (x >= 0) {
+      value = x;
+    } else {
+      negative = true;
+
+      // One does not simply negate MIN_INT64.
+      if (x == std::numeric_limits<int64_t>::min()) {
+        value = uint64_t(1) << 63;
+      } else {
+        value = -int64_t(x);
+      }
+    }
+  }
+
+  constexpr bool operator==(const IU64& other) const {
+    return value == other.value && negative == other.negative;
+  }
+  constexpr bool operator!=(const IU64& other) const {
+    return !(*this == other);
+  }
+
+  constexpr bool operator<(const IU64& other) const {
+    if (negative) {
+      if (other.negative) {
+        // Both negative; we are smaller if absolute value is larger.
+        return value > other.value;
+      } else {
+        // Only we are negative, so we are smaller.
+        return true;
+      }
+    } else {
+      if (other.negative) {
+        // Only the other is negative, so we are larger.
+        return false;
+      } else {
+        // Both positive; we are smaller if absolute value is smaller.
+        return value < other.value;
+      }
+    }
+  }
+  constexpr bool operator<=(const IU64& other) const {
+    return *this < other || *this == other;
+  }
+  constexpr bool operator>(const IU64& other) const {
+    return !(*this <= other);
+  }
+  constexpr bool operator>=(const IU64& other) const {
+    return !(*this < other);
+  }
+};
+
+inline std::ostream& operator<<(std::ostream& os, const IU64& x) {
+  if (x.negative) {
+    os << '-';
+  }
+  return os << x.value;
+}
+
+} // namespace wasm
+
+namespace std {
+
+template<> class numeric_limits<wasm::IU64> {
+public:
+  static constexpr bool is_specialized = true;
+  static constexpr bool is_signed = true;
+  static constexpr bool is_integer = true;
+  static constexpr bool is_exact = true;
+  static constexpr bool has_infinity = false;
+  static constexpr bool has_quiet_NaN = false;
+  static constexpr bool has_signaling_NaN = false;
+  static constexpr float_denorm_style has_denorm = denorm_absent;
+  static constexpr bool has_denorm_loss = false;
+  static constexpr float_round_style round_style = round_toward_zero;
+  static constexpr bool is_iec559 = false;
+  static constexpr bool is_bounded = true;
+  static constexpr bool is_modulo = false;
+  static constexpr int digits = 65;
+  static constexpr int digits10 = 19;
+  static constexpr int max_digits10 = 0;
+  static constexpr int radix = 2;
+  static constexpr int min_exponent = 0;
+  static constexpr int min_exponent10 = 0;
+  static constexpr int max_exponent = 0;
+  static constexpr int max_exponent10 = 0;
+  static constexpr bool traps = false;
+  static constexpr bool tinyness_before = false;
+
+  static constexpr wasm::IU64 min() noexcept {
+    return wasm::IU64(std::numeric_limits<int64_t>::min());
+  }
+  static constexpr wasm::IU64 lowest() noexcept { return min(); }
+  static constexpr wasm::IU64 max() noexcept {
+    return wasm::IU64(std::numeric_limits<uint64_t>::max());
+  }
+};
+
+} // namespace std
+
+#endif // wasm_support_i65_h
diff --git a/src/support/span.h b/src/support/span.h
new file mode 100644
index 0000000..05cdbce
--- /dev/null
+++ b/src/support/span.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2026 WebAssembly Community Group participants
+ *
+ * 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 wasm_support_span_h
+#define wasm_support_span_h
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <limits>
+
+namespace wasm {
+
+// A span of values.
+//
+// Span{min, max} means [min, max], inclusive of both sides. To represent an
+// empty span, we use min > max.
+template<typename T> struct Span {
+  static constexpr T Min = std::numeric_limits<T>::lowest();
+  static constexpr T Max = std::numeric_limits<T>::max();
+
+  T min = Min;
+  T max = Max;
+
+  constexpr Span() = default;
+  constexpr Span(T min, T max) : min(min), max(max) {}
+
+  // Set a single value as possible.
+  void set(T value) { min = max = value; }
+
+  // To represent an empty span, we use min > max, an impossible span.
+  void setEmpty() {
+    *this = empty();
+    assert(isEmpty());
+  }
+
+  bool isEmpty() const { return min > max; }
+
+  static Span<T> empty() { return Span{Max, Min}; }
+
+  void setFull() {
+    *this = Span();
+    assert(isFull());
+  }
+
+  bool isFull() const { return min == Min && max == Max; }
+
+  static Span<T> full() { return Span{}; }
+
+  // Intersect this with another span, returning a (possibly empty) span.
+  Span<T> intersection(const Span& other) const {
+    if (isEmpty() || other.isEmpty()) {
+      return empty();
+    }
+    return Span<T>{std::max(min, other.min), std::min(max, other.max)};
+  }
+
+  // Checks whether two spans have any overlap at all.
+  bool hasOverlap(const Span& other) const {
+    return !intersection(other).isEmpty();
+  }
+
+  // Check whether we contain another span (possibly being equal).
+  bool contains(const Span& other) const {
+    return intersection(other) == other;
+  }
+
+  bool operator==(const Span& other) const {
+    if (isEmpty()) {
+      return other.isEmpty();
+    }
+    return !other.isEmpty() && min == other.min && max == other.max;
+  }
+  bool operator!=(const Span& other) const { return !(*this == other); }
+};
+
+template<typename T>
+inline std::ostream& operator<<(std::ostream& os, const Span<T>& span) {
+  if (span.isEmpty()) {
+    return os << "[empty]";
+  }
+  return os << '[' << span.min << ", " << span.max << ']';
+}
+
+} // namespace wasm
+
+#endif // wasm_support_span_h
diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt
index ab49c49..925880b 100644
--- a/test/gtest/CMakeLists.txt
+++ b/test/gtest/CMakeLists.txt
@@ -17,6 +17,7 @@
   disjoint_sets.cpp
   graph.cpp
   int128.cpp
+  iu64.cpp
   leaves.cpp
   glbs.cpp
   inplace_vector.cpp
@@ -31,6 +32,7 @@
   printing.cpp
   public-type-validator.cpp
   scc.cpp
+  span.cpp
   stringify.cpp
   subtype-exprs.cpp
   suffix_tree.cpp
diff --git a/test/gtest/iu64.cpp b/test/gtest/iu64.cpp
new file mode 100644
index 0000000..1f812e9
--- /dev/null
+++ b/test/gtest/iu64.cpp
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2026 WebAssembly Community Group participants
+ *
+ * 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 <cstdint>
+#include <limits>
+#include <sstream>
+#include <vector>
+
+#include "support/iu64.h"
+#include "gtest/gtest.h"
+
+using namespace wasm;
+
+TEST(IU64Test, DefaultConstruct) {
+  IU64 x;
+  EXPECT_EQ(x.value, 0u);
+  EXPECT_FALSE(x.negative);
+  EXPECT_EQ(x, IU64(0));
+}
+
+TEST(IU64Test, ConstructFromUnsigned32) {
+  uint32_t zero = 0;
+  uint32_t one = 1;
+  uint32_t mid = 12345678;
+  uint32_t maxU32 = std::numeric_limits<uint32_t>::max();
+
+  IU64 iZero(zero);
+  EXPECT_EQ(iZero.value, 0u);
+  EXPECT_FALSE(iZero.negative);
+
+  IU64 iOne(one);
+  EXPECT_EQ(iOne.value, 1u);
+  EXPECT_FALSE(iOne.negative);
+
+  IU64 iMid(mid);
+  EXPECT_EQ(iMid.value, mid);
+  EXPECT_FALSE(iMid.negative);
+
+  IU64 iMax(maxU32);
+  EXPECT_EQ(iMax.value, uint64_t(maxU32));
+  EXPECT_FALSE(iMax.negative);
+}
+
+TEST(IU64Test, ConstructFromSigned32) {
+  int32_t zero = 0;
+  int32_t one = 1;
+  int32_t maxI32 = std::numeric_limits<int32_t>::max();
+  int32_t negOne = -1;
+  int32_t negMid = -12345678;
+  int32_t minI32 = std::numeric_limits<int32_t>::min();
+
+  IU64 iZero(zero);
+  EXPECT_EQ(iZero.value, 0u);
+  EXPECT_FALSE(iZero.negative);
+
+  IU64 iOne(one);
+  EXPECT_EQ(iOne.value, 1u);
+  EXPECT_FALSE(iOne.negative);
+
+  IU64 iMax(maxI32);
+  EXPECT_EQ(iMax.value, uint64_t(maxI32));
+  EXPECT_FALSE(iMax.negative);
+
+  IU64 iNegOne(negOne);
+  EXPECT_EQ(iNegOne.value, 1u);
+  EXPECT_TRUE(iNegOne.negative);
+
+  IU64 iNegMid(negMid);
+  EXPECT_EQ(iNegMid.value, 12345678u);
+  EXPECT_TRUE(iNegMid.negative);
+
+  IU64 iMin(minI32);
+  EXPECT_EQ(iMin.value, 2147483648ULL);
+  EXPECT_TRUE(iMin.negative);
+}
+
+TEST(IU64Test, ConstructFromUnsigned64) {
+  uint64_t zero = 0;
+  uint64_t one = 1;
+  uint64_t maxU32 = std::numeric_limits<uint32_t>::max();
+  uint64_t maxI64 = std::numeric_limits<int64_t>::max();
+  uint64_t highBitOnly = uint64_t(1) << 63;
+  uint64_t maxU64 = std::numeric_limits<uint64_t>::max();
+
+  IU64 iZero(zero);
+  EXPECT_EQ(iZero.value, 0u);
+  EXPECT_FALSE(iZero.negative);
+
+  IU64 iOne(one);
+  EXPECT_EQ(iOne.value, 1u);
+  EXPECT_FALSE(iOne.negative);
+
+  IU64 iMaxU32(maxU32);
+  EXPECT_EQ(iMaxU32.value, maxU32);
+  EXPECT_FALSE(iMaxU32.negative);
+
+  IU64 iMaxI64(maxI64);
+  EXPECT_EQ(iMaxI64.value, maxI64);
+  EXPECT_FALSE(iMaxI64.negative);
+
+  IU64 iHighBit(highBitOnly);
+  EXPECT_EQ(iHighBit.value, highBitOnly);
+  EXPECT_FALSE(iHighBit.negative);
+
+  IU64 iMaxU64(maxU64);
+  EXPECT_EQ(iMaxU64.value, maxU64);
+  EXPECT_FALSE(iMaxU64.negative);
+}
+
+TEST(IU64Test, ConstructFromSigned64) {
+  int64_t zero = 0;
+  int64_t one = 1;
+  int64_t maxI64 = std::numeric_limits<int64_t>::max();
+  int64_t negOne = -1;
+  int64_t minI64 = std::numeric_limits<int64_t>::min();
+  int64_t minI64PlusOne = std::numeric_limits<int64_t>::min() + 1;
+
+  IU64 iZero(zero);
+  EXPECT_EQ(iZero.value, 0u);
+  EXPECT_FALSE(iZero.negative);
+
+  IU64 iOne(one);
+  EXPECT_EQ(iOne.value, 1u);
+  EXPECT_FALSE(iOne.negative);
+
+  IU64 iMax(maxI64);
+  EXPECT_EQ(iMax.value, uint64_t(maxI64));
+  EXPECT_FALSE(iMax.negative);
+
+  IU64 iNegOne(negOne);
+  EXPECT_EQ(iNegOne.value, 1u);
+  EXPECT_TRUE(iNegOne.negative);
+
+  IU64 iMin(minI64);
+  EXPECT_EQ(iMin.value, uint64_t(1) << 63);
+  EXPECT_TRUE(iMin.negative);
+
+  IU64 iMinPlusOne(minI64PlusOne);
+  EXPECT_EQ(iMinPlusOne.value, uint64_t(std::numeric_limits<int64_t>::max()));
+  EXPECT_TRUE(iMinPlusOne.negative);
+}
+
+TEST(IU64Test, EqualityAndInequality) {
+  EXPECT_EQ(IU64(int32_t(0)), IU64(uint32_t(0)));
+  EXPECT_EQ(IU64(int32_t(0)), IU64(int64_t(0)));
+  EXPECT_EQ(IU64(int32_t(0)), IU64(uint64_t(0)));
+
+  EXPECT_EQ(IU64(int32_t(42)), IU64(uint32_t(42)));
+  EXPECT_EQ(IU64(int32_t(42)), IU64(int64_t(42)));
+  EXPECT_EQ(IU64(int32_t(42)), IU64(uint64_t(42)));
+
+  EXPECT_EQ(IU64(int32_t(-42)), IU64(int64_t(-42)));
+  EXPECT_EQ(IU64(std::numeric_limits<int32_t>::min()),
+            IU64(int64_t(std::numeric_limits<int32_t>::min())));
+
+  EXPECT_NE(IU64(int32_t(1)), IU64(int32_t(-1)));
+  EXPECT_NE(IU64(uint64_t(0xffffffffffffffffULL)), IU64(int64_t(-1)));
+  EXPECT_NE(IU64(std::numeric_limits<int64_t>::min()), IU64(uint64_t(1) << 63));
+}
+
+TEST(IU64Test, TotalOrdering) {
+  std::vector<IU64> sortedValues = {
+    IU64(std::numeric_limits<int64_t>::min()),
+    IU64(std::numeric_limits<int64_t>::min() + 1),
+    IU64(int64_t(-0x100000000LL)),
+    IU64(std::numeric_limits<int32_t>::min()),
+    IU64(int32_t(-12345)),
+    IU64(int64_t(-2)),
+    IU64(int64_t(-1)),
+    IU64(0),
+    IU64(1),
+    IU64(2),
+    IU64(int32_t(12345)),
+    IU64(std::numeric_limits<int32_t>::max()),
+    IU64(uint64_t(std::numeric_limits<int32_t>::max()) + 1),
+    IU64(std::numeric_limits<uint32_t>::max()),
+    IU64(uint64_t(std::numeric_limits<uint32_t>::max()) + 1),
+    IU64(std::numeric_limits<int64_t>::max() - 1),
+    IU64(std::numeric_limits<int64_t>::max()),
+    IU64(uint64_t(std::numeric_limits<int64_t>::max()) + 1),
+    IU64(std::numeric_limits<uint64_t>::max() - 1),
+    IU64(std::numeric_limits<uint64_t>::max()),
+  };
+
+  for (size_t i = 0; i < sortedValues.size(); ++i) {
+    for (size_t j = 0; j < sortedValues.size(); ++j) {
+      const auto& a = sortedValues[i];
+      const auto& b = sortedValues[j];
+
+      if (i < j) {
+        EXPECT_LT(a, b);
+        EXPECT_LE(a, b);
+        EXPECT_GT(b, a);
+        EXPECT_GE(b, a);
+        EXPECT_NE(a, b);
+        EXPECT_FALSE(a == b);
+        EXPECT_FALSE(b < a);
+      } else if (i == j) {
+        EXPECT_EQ(a, b);
+        EXPECT_LE(a, b);
+        EXPECT_GE(a, b);
+        EXPECT_FALSE(a < b);
+        EXPECT_FALSE(a > b);
+        EXPECT_FALSE(a != b);
+      } else {
+        EXPECT_GT(a, b);
+        EXPECT_GE(a, b);
+        EXPECT_LT(b, a);
+        EXPECT_LE(b, a);
+        EXPECT_NE(a, b);
+        EXPECT_FALSE(a == b);
+        EXPECT_FALSE(a < b);
+      }
+    }
+  }
+}
+
+TEST(IU64Test, NumericLimits) {
+  EXPECT_TRUE(std::numeric_limits<IU64>::is_specialized);
+  EXPECT_TRUE(std::numeric_limits<IU64>::is_signed);
+  EXPECT_TRUE(std::numeric_limits<IU64>::is_integer);
+
+  EXPECT_EQ(std::numeric_limits<IU64>::min(),
+            IU64(std::numeric_limits<int64_t>::min()));
+  EXPECT_EQ(std::numeric_limits<IU64>::lowest(),
+            IU64(std::numeric_limits<int64_t>::min()));
+  EXPECT_EQ(std::numeric_limits<IU64>::max(),
+            IU64(std::numeric_limits<uint64_t>::max()));
+}
+
+TEST(IU64Test, StreamOutput) {
+  auto toString = [](const IU64& x) {
+    std::ostringstream ss;
+    ss << x;
+    return ss.str();
+  };
+
+  EXPECT_EQ(toString(IU64(0)), "0");
+  EXPECT_EQ(toString(IU64(42)), "42");
+  EXPECT_EQ(toString(IU64(-42)), "-42");
+  EXPECT_EQ(toString(IU64(std::numeric_limits<int64_t>::min())),
+            "-9223372036854775808");
+  EXPECT_EQ(toString(IU64(std::numeric_limits<uint64_t>::max())),
+            "18446744073709551615");
+}
diff --git a/test/gtest/span.cpp b/test/gtest/span.cpp
new file mode 100644
index 0000000..f05fd80
--- /dev/null
+++ b/test/gtest/span.cpp
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2026 WebAssembly Community Group participants
+ *
+ * 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 <cstdint>
+#include <limits>
+#include <sstream>
+
+#include "support/iu64.h"
+#include "support/span.h"
+#include "gtest/gtest.h"
+
+using namespace wasm;
+
+// ============================================================================
+// Generic Span<T> tests
+// ============================================================================
+
+TEST(SpanTest, EmptySpanInt) {
+  Span<int32_t> empty = Span<int32_t>::empty();
+  EXPECT_TRUE(empty.isEmpty());
+  EXPECT_FALSE(empty.isFull());
+
+  Span<int32_t> invalid(10, 5);
+  EXPECT_TRUE(invalid.isEmpty());
+  EXPECT_FALSE(invalid.isFull());
+
+  EXPECT_EQ(empty, invalid);
+
+  Span<int32_t> s;
+  EXPECT_FALSE(s.isEmpty());
+  s.setEmpty();
+  EXPECT_TRUE(s.isEmpty());
+  EXPECT_EQ(s, empty);
+}
+
+TEST(SpanTest, FullSpanIntTypes) {
+  // Signed 32-bit
+  Span<int32_t> fullI32 = Span<int32_t>::full();
+  EXPECT_TRUE(fullI32.isFull());
+  EXPECT_FALSE(fullI32.isEmpty());
+  EXPECT_EQ(fullI32.min, std::numeric_limits<int32_t>::min());
+  EXPECT_EQ(fullI32.max, std::numeric_limits<int32_t>::max());
+
+  Span<int32_t> defI32;
+  EXPECT_TRUE(defI32.isFull());
+  EXPECT_EQ(defI32, fullI32);
+
+  // Unsigned 32-bit
+  Span<uint32_t> fullU32 = Span<uint32_t>::full();
+  EXPECT_TRUE(fullU32.isFull());
+  EXPECT_FALSE(fullU32.isEmpty());
+  EXPECT_EQ(fullU32.min, 0u);
+  EXPECT_EQ(fullU32.max, std::numeric_limits<uint32_t>::max());
+
+  // Signed 64-bit
+  Span<int64_t> fullI64 = Span<int64_t>::full();
+  EXPECT_TRUE(fullI64.isFull());
+  EXPECT_FALSE(fullI64.isEmpty());
+  EXPECT_EQ(fullI64.min, std::numeric_limits<int64_t>::min());
+  EXPECT_EQ(fullI64.max, std::numeric_limits<int64_t>::max());
+
+  // Unsigned 64-bit
+  Span<uint64_t> fullU64 = Span<uint64_t>::full();
+  EXPECT_TRUE(fullU64.isFull());
+  EXPECT_FALSE(fullU64.isEmpty());
+  EXPECT_EQ(fullU64.min, 0ull);
+  EXPECT_EQ(fullU64.max, std::numeric_limits<uint64_t>::max());
+}
+
+TEST(SpanTest, SetSingleValue) {
+  Span<int32_t> s;
+  s.set(42);
+  EXPECT_EQ(s.min, 42);
+  EXPECT_EQ(s.max, 42);
+  EXPECT_FALSE(s.isEmpty());
+  EXPECT_FALSE(s.isFull());
+  EXPECT_EQ(s, Span<int32_t>(42, 42));
+}
+
+TEST(SpanTest, SetFull) {
+  Span<int32_t> s(10, 20);
+  EXPECT_FALSE(s.isFull());
+  s.setFull();
+  EXPECT_TRUE(s.isFull());
+  EXPECT_EQ(s.min, std::numeric_limits<int32_t>::min());
+  EXPECT_EQ(s.max, std::numeric_limits<int32_t>::max());
+}
+
+TEST(SpanTest, IntersectionInt) {
+  Span<int32_t> a(1, 10);
+  Span<int32_t> b(5, 15);
+  Span<int32_t> ab = a.intersection(b);
+  EXPECT_EQ(ab, Span<int32_t>(5, 10));
+
+  // Commutativity
+  EXPECT_EQ(b.intersection(a), Span<int32_t>(5, 10));
+
+  // Touching at a single point
+  Span<int32_t> c(10, 20);
+  EXPECT_EQ(a.intersection(c), Span<int32_t>(10, 10));
+
+  // Disjoint
+  Span<int32_t> d(11, 20);
+  EXPECT_TRUE(a.intersection(d).isEmpty());
+  EXPECT_EQ(a.intersection(d), Span<int32_t>::empty());
+
+  // Contained
+  Span<int32_t> e(3, 7);
+  EXPECT_EQ(a.intersection(e), Span<int32_t>(3, 7));
+
+  // Identical
+  EXPECT_EQ(a.intersection(a), a);
+
+  // With empty
+  EXPECT_TRUE(a.intersection(Span<int32_t>::empty()).isEmpty());
+  EXPECT_TRUE(Span<int32_t>::empty().intersection(a).isEmpty());
+
+  // With full
+  EXPECT_EQ(a.intersection(Span<int32_t>::full()), a);
+  EXPECT_EQ(Span<int32_t>::full().intersection(a), a);
+}
+
+TEST(SpanTest, HasOverlapInt) {
+  Span<int32_t> a(1, 10);
+  Span<int32_t> b(5, 15);
+  Span<int32_t> c(10, 20);
+  Span<int32_t> d(11, 20);
+
+  EXPECT_TRUE(a.hasOverlap(b));
+  EXPECT_TRUE(b.hasOverlap(a));
+  EXPECT_TRUE(a.hasOverlap(c));
+  EXPECT_FALSE(a.hasOverlap(d));
+  EXPECT_FALSE(d.hasOverlap(a));
+
+  EXPECT_FALSE(a.hasOverlap(Span<int32_t>::empty()));
+  EXPECT_TRUE(a.hasOverlap(Span<int32_t>::full()));
+  EXPECT_FALSE(Span<int32_t>::empty().hasOverlap(Span<int32_t>::full()));
+}
+
+TEST(SpanTest, ContainsInt) {
+  Span<int32_t> a(1, 10);
+  Span<int32_t> b(3, 7);
+  Span<int32_t> c(5, 15);
+  Span<int32_t> d(11, 20);
+
+  EXPECT_TRUE(a.contains(b));
+  EXPECT_FALSE(b.contains(a));
+
+  EXPECT_TRUE(a.contains(a));
+  EXPECT_FALSE(a.contains(c));
+  EXPECT_FALSE(a.contains(d));
+
+  EXPECT_TRUE(a.contains(Span<int32_t>::empty()));
+  EXPECT_TRUE(Span<int32_t>::empty().contains(Span<int32_t>::empty()));
+  EXPECT_FALSE(Span<int32_t>::empty().contains(a));
+
+  EXPECT_TRUE(Span<int32_t>::full().contains(a));
+  EXPECT_TRUE(Span<int32_t>::full().contains(Span<int32_t>::empty()));
+  EXPECT_FALSE(a.contains(Span<int32_t>::full()));
+}
+
+TEST(SpanTest, StreamOutput) {
+  auto toString = [](const auto& span) {
+    std::ostringstream ss;
+    ss << span;
+    return ss.str();
+  };
+
+  EXPECT_EQ(toString(Span<int32_t>(1, 10)), "[1, 10]");
+  EXPECT_EQ(toString(Span<int32_t>::empty()), "[empty]");
+  EXPECT_EQ(toString(Span<int32_t>(10, 5)), "[empty]");
+}
+
+// ============================================================================
+// Span<IU64> tests (corner cases, sign mixing, large range)
+// ============================================================================
+
+TEST(SpanIU64Test, FullAndLimits) {
+  EXPECT_EQ(Span<IU64>::Min, IU64(std::numeric_limits<int64_t>::min()));
+  EXPECT_EQ(Span<IU64>::Max, IU64(std::numeric_limits<uint64_t>::max()));
+
+  Span<IU64> full = Span<IU64>::full();
+  EXPECT_TRUE(full.isFull());
+  EXPECT_FALSE(full.isEmpty());
+  EXPECT_EQ(full.min, IU64(std::numeric_limits<int64_t>::min()));
+  EXPECT_EQ(full.max, IU64(std::numeric_limits<uint64_t>::max()));
+
+  // Default constructed span is full
+  Span<IU64> def;
+  EXPECT_TRUE(def.isFull());
+  EXPECT_FALSE(def.isEmpty());
+  EXPECT_EQ(def, full);
+}
+
+TEST(SpanIU64Test, Empty) {
+  Span<IU64> empty = Span<IU64>::empty();
+  EXPECT_TRUE(empty.isEmpty());
+  EXPECT_FALSE(empty.isFull());
+
+  Span<IU64> empty2{IU64(100), IU64(-100)};
+  EXPECT_TRUE(empty2.isEmpty());
+  EXPECT_FALSE(empty2.isFull());
+  EXPECT_EQ(empty, empty2);
+
+  Span<IU64> empty3{IU64(uint64_t(1)), IU64(int64_t(-1))};
+  EXPECT_TRUE(empty3.isEmpty());
+  EXPECT_EQ(empty, empty3);
+}
+
+TEST(SpanIU64Test, SingletonsAtExtremes) {
+  // Min int64 singleton
+  Span<IU64> minI64{IU64(std::numeric_limits<int64_t>::min()),
+                    IU64(std::numeric_limits<int64_t>::min())};
+  EXPECT_FALSE(minI64.isEmpty());
+  EXPECT_FALSE(minI64.isFull());
+  EXPECT_EQ(minI64.min, IU64(std::numeric_limits<int64_t>::min()));
+  EXPECT_EQ(minI64.max, IU64(std::numeric_limits<int64_t>::min()));
+
+  // -1 singleton
+  Span<IU64> negOne{IU64(-1), IU64(-1)};
+  EXPECT_FALSE(negOne.isEmpty());
+
+  // 0 singleton
+  Span<IU64> zero{IU64(0), IU64(0)};
+  EXPECT_FALSE(zero.isEmpty());
+
+  // 1 singleton
+  Span<IU64> one{IU64(1), IU64(1)};
+  EXPECT_FALSE(one.isEmpty());
+
+  // Max int64 singleton
+  Span<IU64> maxI64{IU64(std::numeric_limits<int64_t>::max()),
+                    IU64(std::numeric_limits<int64_t>::max())};
+  EXPECT_FALSE(maxI64.isEmpty());
+
+  // 2^63 singleton (above int64_t max, into uint64_t territory)
+  Span<IU64> highBit{IU64(uint64_t(1) << 63), IU64(uint64_t(1) << 63)};
+  EXPECT_FALSE(highBit.isEmpty());
+
+  // Max uint64 singleton
+  Span<IU64> maxU64{IU64(std::numeric_limits<uint64_t>::max()),
+                    IU64(std::numeric_limits<uint64_t>::max())};
+  EXPECT_FALSE(maxU64.isEmpty());
+}
+
+TEST(SpanIU64Test, CrossingZero) {
+  Span<IU64> span{IU64(-10), IU64(10)};
+  EXPECT_FALSE(span.isEmpty());
+  EXPECT_FALSE(span.isFull());
+
+  // Contains points inside
+  EXPECT_TRUE(span.contains(Span<IU64>(IU64(-10), IU64(-10))));
+  EXPECT_TRUE(span.contains(Span<IU64>(IU64(-5), IU64(5))));
+  EXPECT_TRUE(span.contains(Span<IU64>(IU64(0), IU64(0))));
+  EXPECT_TRUE(span.contains(Span<IU64>(IU64(10), IU64(10))));
+
+  // Does not contain points outside
+  EXPECT_FALSE(span.contains(Span<IU64>(IU64(-11), IU64(-11))));
+  EXPECT_FALSE(span.contains(Span<IU64>(IU64(11), IU64(11))));
+  EXPECT_FALSE(span.contains(Span<IU64>(IU64(-15), IU64(5))));
+  EXPECT_FALSE(span.contains(Span<IU64>(IU64(-5), IU64(15))));
+}
+
+TEST(SpanIU64Test, NegativeAndPositiveIntersections) {
+  Span<IU64> neg{IU64(-100), IU64(-10)};
+  Span<IU64> pos{IU64(10), IU64(100)};
+
+  EXPECT_FALSE(neg.hasOverlap(pos));
+  EXPECT_FALSE(pos.hasOverlap(neg));
+  EXPECT_TRUE(neg.intersection(pos).isEmpty());
+  EXPECT_TRUE(pos.intersection(neg).isEmpty());
+
+  Span<IU64> touchNegZero{IU64(-10), IU64(0)};
+  Span<IU64> touchZeroPos{IU64(0), IU64(10)};
+  EXPECT_TRUE(touchNegZero.hasOverlap(touchZeroPos));
+  EXPECT_EQ(touchNegZero.intersection(touchZeroPos),
+            Span<IU64>(IU64(0), IU64(0)));
+
+  Span<IU64> overlap{IU64(-50), IU64(50)};
+  EXPECT_EQ(neg.intersection(overlap), Span<IU64>(IU64(-50), IU64(-10)));
+  EXPECT_EQ(pos.intersection(overlap), Span<IU64>(IU64(10), IU64(50)));
+}
+
+TEST(SpanIU64Test, SignedUnsignedBoundary) {
+  // Test around INT64_MAX and 2^63
+  int64_t maxI64 = std::numeric_limits<int64_t>::max();
+  uint64_t highBit = uint64_t(maxI64) + 1; // 0x8000000000000000ULL
+
+  Span<IU64> s1(IU64(maxI64 - 100), IU64(highBit + 50));
+  Span<IU64> s2(IU64(highBit), IU64(highBit + 100));
+
+  EXPECT_TRUE(s1.hasOverlap(s2));
+  EXPECT_EQ(s1.intersection(s2), Span<IU64>(IU64(highBit), IU64(highBit + 50)));
+
+  // Disjoint near 2^63 boundary
+  Span<IU64> s3{IU64(maxI64 - 200), IU64(maxI64)};
+  Span<IU64> s4{IU64(highBit + 1), IU64(highBit + 100)};
+  EXPECT_FALSE(s3.hasOverlap(s4));
+  EXPECT_TRUE(s3.intersection(s4).isEmpty());
+
+  // Adjacent touching at 2^63
+  Span<IU64> s5{IU64(maxI64), IU64(highBit)};
+  Span<IU64> s6{IU64(highBit), IU64(highBit + 10)};
+  EXPECT_TRUE(s5.hasOverlap(s6));
+  EXPECT_EQ(s5.intersection(s6), Span<IU64>(IU64(highBit), IU64(highBit)));
+}
+
+TEST(SpanIU64Test, ExtremeBoundaries) {
+  Span<IU64> minPart(IU64(std::numeric_limits<int64_t>::min()),
+                     IU64(std::numeric_limits<int64_t>::min() + 100));
+  Span<IU64> maxPart(IU64(std::numeric_limits<uint64_t>::max() - 100),
+                     IU64(std::numeric_limits<uint64_t>::max()));
+
+  EXPECT_FALSE(minPart.hasOverlap(maxPart));
+  EXPECT_TRUE(minPart.intersection(maxPart).isEmpty());
+
+  Span<IU64> full = Span<IU64>::full();
+  EXPECT_TRUE(full.contains(minPart));
+  EXPECT_TRUE(full.contains(maxPart));
+  EXPECT_EQ(full.intersection(minPart), minPart);
+  EXPECT_EQ(full.intersection(maxPart), maxPart);
+  EXPECT_TRUE(full.hasOverlap(minPart));
+  EXPECT_TRUE(full.hasOverlap(maxPart));
+
+  Span<IU64> allNeg(IU64(std::numeric_limits<int64_t>::min()), IU64(-1));
+  Span<IU64> allNonNeg(IU64(0), IU64(std::numeric_limits<uint64_t>::max()));
+
+  EXPECT_FALSE(allNeg.hasOverlap(allNonNeg));
+  EXPECT_TRUE(allNeg.intersection(allNonNeg).isEmpty());
+  EXPECT_TRUE(full.contains(allNeg));
+  EXPECT_TRUE(full.contains(allNonNeg));
+}
+
+TEST(SpanIU64Test, SetAndMutate) {
+  Span<IU64> s;
+  EXPECT_TRUE(s.isFull());
+
+  s.set(IU64(-12345));
+  EXPECT_FALSE(s.isFull());
+  EXPECT_FALSE(s.isEmpty());
+  EXPECT_EQ(s.min, IU64(-12345));
+  EXPECT_EQ(s.max, IU64(-12345));
+
+  s.setEmpty();
+  EXPECT_TRUE(s.isEmpty());
+
+  s.setFull();
+  EXPECT_TRUE(s.isFull());
+}