diff --git a/libcxx/docs/Status/RangesPaper.csv b/libcxx/docs/Status/RangesPaper.csv
--- a/libcxx/docs/Status/RangesPaper.csv
+++ b/libcxx/docs/Status/RangesPaper.csv
@@ -155,7 +155,7 @@
 `[range.transform] <https://wg21.link/range.transform>`_,`transform_view <https://llvm.org/D103056>`_,[range.all],Zoe Carver,✅
 `[range.take] <https://wg21.link/range.take>`_,`take_view <https://llvm.org/D106507>`_,[range.all],Zoe Carver,✅
 `[range.join] <https://wg21.link/range.join>`_,`join_view <https://llvm.org/D107671>`_,[range.all],Zoe Carver,✅
-`[range.split] <https://wg21.link/range.split>`_,`split_view (renamed to lazy_split_view by P2210) <https://llvm.org/D107500>`_,[range.all],Zoe Carver,In progress
+`[range.split] <https://wg21.link/range.split>`_,`split_view (renamed to lazy_split_view by P2210) <https://llvm.org/D107500>`_,[range.all],Zoe Carver and Konstantin Varlamov,✅
 `[range.counted] <https://wg21.link/range.counted>`_,`view::counted <https://llvm.org/D106923>`_,[range.subrange],Zoe Carver,✅
 `[range.common] <https://wg21.link/range.common>`_,`common_view <https://llvm.org/D105753>`_,[range.all],Zoe Carver,✅
 `[range.reverse] <https://wg21.link/range.reverse>`_,`reverse_view <https://llvm.org/D107096>`_,[range.all],Zoe Carver,✅
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -354,6 +354,7 @@
   __ranges/enable_view.h
   __ranges/iota_view.h
   __ranges/join_view.h
+  __ranges/lazy_split_view.h
   __ranges/non_propagating_cache.h
   __ranges/owning_view.h
   __ranges/range_adaptor.h
diff --git a/libcxx/include/__ranges/lazy_split_view.h b/libcxx/include/__ranges/lazy_split_view.h
new file mode 100644
--- /dev/null
+++ b/libcxx/include/__ranges/lazy_split_view.h
@@ -0,0 +1,418 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#ifndef _LIBCPP___RANGES_LAZY_SPLIT_VIEW_H
+#define _LIBCPP___RANGES_LAZY_SPLIT_VIEW_H
+
+#include <__algorithm/ranges_find.h>
+#include <__algorithm/ranges_mismatch.h>
+#include <__concepts/constructible.h>
+#include <__config>
+#include <__functional/ranges_operations.h>
+#include <__iterator/concepts.h>
+#include <__iterator/default_sentinel.h>
+#include <__iterator/incrementable_traits.h>
+#include <__iterator/indirectly_comparable.h>
+#include <__iterator/iterator_traits.h>
+#include <__iterator/iter_swap.h>
+#include <__memory/addressof.h>
+#include <__ranges/access.h>
+#include <__ranges/all.h>
+#include <__ranges/concepts.h>
+#include <__ranges/non_propagating_cache.h>
+#include <__ranges/range_adaptor.h>
+#include <__ranges/single_view.h>
+#include <__ranges/view_interface.h>
+#include <__utility/move.h>
+#include <type_traits>
+
+#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#pragma GCC system_header
+#endif
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+#if _LIBCPP_STD_VER > 17 && !defined(_LIBCPP_HAS_NO_RANGES)
+
+namespace ranges {
+
+template <auto> struct __require_constant;
+
+template <class _Range>
+concept __tiny_range =
+  sized_range<_Range> &&
+  requires { typename __require_constant<remove_reference_t<_Range>::size()>; } &&
+  (remove_reference_t<_Range>::size() <= 1);
+
+template <input_range _View, forward_range _Pattern>
+  requires view<_View> && view<_Pattern> &&
+            indirectly_comparable<iterator_t<_View>, iterator_t<_Pattern>, ranges::equal_to> &&
+            (forward_range<_View> || __tiny_range<_Pattern>)
+class lazy_split_view : public view_interface<lazy_split_view<_View, _Pattern>> {
+
+  _View __base_ = _View();
+  _Pattern __pattern_ = _Pattern();
+
+  using _MaybeCurrent = _If<!forward_range<_View>, __non_propagating_cache<iterator_t<_View>>, __empty_cache>;
+  _LIBCPP_NO_UNIQUE_ADDRESS _MaybeCurrent __current_ = _MaybeCurrent();
+
+  template <bool> struct __outer_iterator;
+  template <bool> struct __inner_iterator;
+
+public:
+  _LIBCPP_HIDE_FROM_ABI
+  lazy_split_view()
+    requires default_initializable<_View> && default_initializable<_Pattern> = default;
+
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr lazy_split_view(_View __base, _Pattern __pattern)
+    : __base_(std::move(__base)), __pattern_(std::move(__pattern)) {}
+
+  template <input_range _Range>
+    requires constructible_from<_View, views::all_t<_Range>> &&
+             constructible_from<_Pattern, single_view<range_value_t<_Range>>>
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr lazy_split_view(_Range&& __r, range_value_t<_Range> __e)
+    : __base_(views::all(std::forward<_Range>(__r)))
+    // TODO(var-const): use `views::single` once it's implemented.
+    , __pattern_(ranges::single_view(std::move(__e))) {}
+
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr _View base() const& requires copy_constructible<_View> { return __base_; }
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr _View base() && { return std::move(__base_); }
+
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr auto begin() {
+    if constexpr (forward_range<_View>) {
+      return __outer_iterator<__simple_view<_View> && __simple_view<_Pattern>>{*this, ranges::begin(__base_)};
+    } else {
+      __current_.__emplace(ranges::begin(__base_));
+      return __outer_iterator<false>{*this};
+    }
+  }
+
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr auto begin() const requires forward_range<_View> && forward_range<const _View> {
+    return __outer_iterator<true>{*this, ranges::begin(__base_)};
+  }
+
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr auto end() requires forward_range<_View> && common_range<_View> {
+    return __outer_iterator<__simple_view<_View> && __simple_view<_Pattern>>{*this, ranges::end(__base_)};
+  }
+
+  _LIBCPP_HIDE_FROM_ABI
+  constexpr auto end() const {
+    if constexpr (forward_range<_View> && forward_range<const _View> && common_range<const _View>) {
+      return __outer_iterator<true>{*this, ranges::end(__base_)};
+    } else {
+      return default_sentinel;
+    }
+  }
+
+private:
+
+  template <class>
+  struct __outer_iterator_category {};
+
+  template <forward_range _Tp>
+  struct __outer_iterator_category<_Tp> {
+    using iterator_category = input_iterator_tag;
+  };
+
+  template <bool _Const>
+  struct __outer_iterator : __outer_iterator_category<__maybe_const<_Const, _View>> {
+  private:
+    template <bool>
+    friend struct __inner_iterator;
+    friend __outer_iterator<true>;
+
+    using _Parent = __maybe_const<_Const, lazy_split_view>;
+    using _Base = __maybe_const<_Const, _View>;
+
+    _Parent* __parent_ = nullptr;
+    using _MaybeCurrent = _If<forward_range<_View>, iterator_t<_Base>, __empty_cache>;
+    _LIBCPP_NO_UNIQUE_ADDRESS _MaybeCurrent __current_ = _MaybeCurrent();
+    bool __trailing_empty_ = false;
+
+    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
+    constexpr auto& __current() noexcept {
+      if constexpr (forward_range<_View>) {
+        return __current_;
+      } else {
+        return *__parent_->__current_;
+      }
+    }
+
+    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
+    constexpr auto& __current() const noexcept {
+      if constexpr (forward_range<_View>) {
+        return __current_;
+      } else {
+        return *__parent_->__current_;
+      }
+    }
+
+  public:
+    // using iterator_category = inherited;
+    using iterator_concept = conditional_t<forward_range<_Base>, forward_iterator_tag, input_iterator_tag>;
+    using difference_type = range_difference_t<_Base>;
+
+    struct value_type : view_interface<value_type> {
+    private:
+      __outer_iterator __i_ = __outer_iterator();
+
+    public:
+      _LIBCPP_HIDE_FROM_ABI
+      value_type() = default;
+      _LIBCPP_HIDE_FROM_ABI
+      constexpr explicit value_type(__outer_iterator __i)
+        : __i_(std::move(__i)) {}
+
+      _LIBCPP_HIDE_FROM_ABI
+      constexpr __inner_iterator<_Const> begin() const { return __inner_iterator<_Const>{__i_}; }
+      _LIBCPP_HIDE_FROM_ABI
+      constexpr default_sentinel_t end() const noexcept { return default_sentinel; }
+    };
+
+    _LIBCPP_HIDE_FROM_ABI
+    __outer_iterator() = default;
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr explicit __outer_iterator(_Parent& __parent)
+      requires (!forward_range<_Base>)
+      : __parent_(std::addressof(__parent)) {}
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr __outer_iterator(_Parent& __parent, iterator_t<_Base> __current)
+      requires forward_range<_Base>
+      : __parent_(std::addressof(__parent)), __current_(std::move(__current)) {}
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr __outer_iterator(__outer_iterator<!_Const> __i)
+      requires _Const && convertible_to<iterator_t<_View>, iterator_t<_Base>>
+      : __parent_(__i.__parent_), __current_(std::move(__i.__current_)) {}
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr value_type operator*() const { return value_type{*this}; }
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr __outer_iterator& operator++() {
+      const auto __end = ranges::end(__parent_->__base_);
+      if (__current() == __end) {
+        __trailing_empty_ = false;
+        return *this;
+      }
+
+      const auto [__pbegin, __pend] = ranges::subrange{__parent_->__pattern_};
+      if (__pbegin == __pend) {
+        ++__current();
+
+      } else if constexpr (__tiny_range<_Pattern>) {
+        __current() = ranges::find(std::move(__current()), __end, *__pbegin);
+        if (__current() != __end) {
+          ++__current();
+          if (__current() == __end)
+            __trailing_empty_ = true;
+        }
+
+      } else {
+        do {
+          const auto [__b, __p] = std::ranges::mismatch(__current(), __end, __pbegin, __pend);
+          if (__p == __pend) {
+            __current() = __b;
+            if (__current() == __end) {
+              __trailing_empty_ = true;
+            }
+            break; // The pattern matched; skip it.
+          }
+        } while (++__current() != __end);
+      }
+
+      return *this;
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr decltype(auto) operator++(int) {
+      if constexpr (forward_range<_Base>) {
+        auto __tmp = *this;
+        ++*this;
+        return __tmp;
+
+      } else {
+        ++*this;
+      }
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    friend constexpr bool operator==(const __outer_iterator& __x, const __outer_iterator& __y)
+      requires forward_range<_Base> {
+      return __x.__current_ == __y.__current_ && __x.__trailing_empty_ == __y.__trailing_empty_;
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    friend constexpr bool operator==(const __outer_iterator& __x, default_sentinel_t) {
+      return __x.__current() == ranges::end(__x.__parent_->__base_) && !__x.__trailing_empty_;
+    }
+  };
+
+  template <class>
+  struct __inner_iterator_category {};
+
+  template <forward_range _Tp>
+  struct __inner_iterator_category<_Tp> {
+    using iterator_category = _If<
+      derived_from<typename iterator_traits<iterator_t<_Tp>>::iterator_category, forward_iterator_tag>,
+      forward_iterator_tag,
+      typename iterator_traits<iterator_t<_Tp>>::iterator_category
+    >;
+  };
+
+  template <bool _Const>
+  struct __inner_iterator : __inner_iterator_category<__maybe_const<_Const, _View>> {
+  private:
+    using _Base = __maybe_const<_Const, _View>;
+    __outer_iterator<_Const> __i_ = __outer_iterator<_Const>();
+    bool __incremented_ = false;
+
+  public:
+    // using iterator_category = inherited;
+    using iterator_concept = typename __outer_iterator<_Const>::iterator_concept;
+    using value_type = range_value_t<_Base>;
+    using difference_type = range_difference_t<_Base>;
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr __inner_iterator() = default;
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr explicit __inner_iterator(__outer_iterator<_Const> __i)
+      : __i_(std::move(__i)) {}
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr const iterator_t<_Base>& base() const& noexcept { return __i_.__current(); }
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr iterator_t<_Base> base() &&
+      requires forward_range<_View> { return std::move(__i_.__current()); }
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr decltype(auto) operator*() const { return *__i_.__current(); }
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr __inner_iterator& operator++() {
+      __incremented_ = true;
+
+      if constexpr (!forward_range<_Base>) {
+        if constexpr (_Pattern::size() == 0) {
+          return *this;
+        }
+      }
+
+      ++__i_.__current();
+      return *this;
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    constexpr decltype(auto) operator++(int) {
+      if constexpr (forward_range<_Base>) {
+        auto __tmp = *this;
+        ++*this;
+        return __tmp;
+
+      } else {
+        ++*this;
+      }
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    friend constexpr bool operator==(const __inner_iterator& __x, const __inner_iterator& __y)
+      requires forward_range<_Base> {
+      return __x.__i_.__current() == __y.__i_.__current();
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    friend constexpr bool operator==(const __inner_iterator& __x, default_sentinel_t) {
+      auto [__pcur, __pend] = ranges::subrange{__x.__i_.__parent_->__pattern_};
+      auto __end = ranges::end(__x.__i_.__parent_->__base_);
+
+      if constexpr (__tiny_range<_Pattern>) {
+        const auto& __cur = __x.__i_.__current();
+        if (__cur == __end) return true;
+        if (__pcur == __pend)  return __x.__incremented_;
+
+        return *__cur == *__pcur;
+
+      } else {
+        auto __cur = __x.__i_.__current();
+        if (__cur == __end) return true;
+        if (__pcur == __pend)  return __x.__incremented_;
+
+        do {
+          if (*__cur != *__pcur) return false;
+          if (++__pcur == __pend) return true;
+        } while (++__cur != __end);
+
+        return false;
+      }
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    friend constexpr decltype(auto) iter_move(const __inner_iterator& __i)
+        noexcept(noexcept(ranges::iter_move(__i.__i_.__current()))) {
+      return ranges::iter_move(__i.__i_.__current());
+    }
+
+    _LIBCPP_HIDE_FROM_ABI
+    friend constexpr void iter_swap(const __inner_iterator& __x, const __inner_iterator& __y)
+        noexcept(noexcept(ranges::iter_swap(__x.__i_.__current(), __y.__i_.__current())))
+        requires indirectly_swappable<iterator_t<_Base>> {
+      ranges::iter_swap(__x.__i_.__current(), __y.__i_.__current());
+    }
+  };
+
+};
+
+template <class _Range, class _Pattern>
+lazy_split_view(_Range&&, _Pattern&&) -> lazy_split_view<views::all_t<_Range>, views::all_t<_Pattern>>;
+
+template <input_range _Range>
+lazy_split_view(_Range&&, range_value_t<_Range>)
+  -> lazy_split_view<views::all_t<_Range>, single_view<range_value_t<_Range>>>;
+
+namespace views {
+namespace __lazy_split_view {
+struct __fn : __range_adaptor_closure<__fn> {
+  template <class _Range, class _Pattern>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
+  constexpr auto operator()(_Range&& __range, _Pattern&& __pattern) const
+    noexcept(noexcept(lazy_split_view(std::forward<_Range>(__range), std::forward<_Pattern>(__pattern))))
+    -> decltype(      lazy_split_view(std::forward<_Range>(__range), std::forward<_Pattern>(__pattern)))
+    { return          lazy_split_view(std::forward<_Range>(__range), std::forward<_Pattern>(__pattern)); }
+
+  template <class _Pattern>
+    requires constructible_from<decay_t<_Pattern>, _Pattern>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
+  constexpr auto operator()(_Pattern&& __pattern) const
+      noexcept(is_nothrow_constructible_v<decay_t<_Pattern>, _Pattern>) {
+    return __range_adaptor_closure_t(std::__bind_back(*this, std::forward<_Pattern>(__pattern)));
+  }
+};
+} // namespace __lazy_split_view
+
+inline namespace __cpo {
+  inline constexpr auto lazy_split = __lazy_split_view::__fn{};
+} // namespace __cpo
+} // namespace views
+
+} // namespace ranges
+
+#endif // _LIBCPP_STD_VER > 17 && !defined(_LIBCPP_HAS_NO_RANGES)
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // _LIBCPP___RANGES_LAZY_SPLIT_VIEW_H
diff --git a/libcxx/include/module.modulemap b/libcxx/include/module.modulemap
--- a/libcxx/include/module.modulemap
+++ b/libcxx/include/module.modulemap
@@ -828,6 +828,7 @@
       module enable_view            { private header "__ranges/enable_view.h" }
       module iota_view              { private header "__ranges/iota_view.h" }
       module join_view              { private header "__ranges/join_view.h" }
+      module lazy_split_view        { private header "__ranges/lazy_split_view.h" }
       module non_propagating_cache  { private header "__ranges/non_propagating_cache.h" }
       module owning_view            { private header "__ranges/owning_view.h" }
       module range_adaptor          { private header "__ranges/range_adaptor.h" }
diff --git a/libcxx/include/ranges b/libcxx/include/ranges
--- a/libcxx/include/ranges
+++ b/libcxx/include/ranges
@@ -196,6 +196,20 @@
   template<input_range V>
     requires view<V> && input_range<range_reference_t<V>>
   class join_view;
+
+  // [range.lazy.split], lazy split view
+  template<class R>
+    concept tiny-range = see below;   // exposition only
+
+  template<input_range V, forward_range Pattern>
+    requires view<V> && view<Pattern> &&
+             indirectly_comparable<iterator_t<V>, iterator_t<Pattern>, ranges::equal_to> &&
+             (forward_range<V> || tiny-range<Pattern>)
+  class lazy_split_view;
+
+  namespace views {
+    inline constexpr unspecified lazy_split = unspecified;
+  }
 }
 
 namespace std {
@@ -245,6 +259,7 @@
 #include <__ranges/enable_view.h>
 #include <__ranges/iota_view.h>
 #include <__ranges/join_view.h>
+#include <__ranges/lazy_split_view.h>
 #include <__ranges/rbegin.h>
 #include <__ranges/ref_view.h>
 #include <__ranges/rend.h>
diff --git a/libcxx/test/libcxx/diagnostics/detail.headers/ranges/lazy_split_view.module.verify.cpp b/libcxx/test/libcxx/diagnostics/detail.headers/ranges/lazy_split_view.module.verify.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/libcxx/diagnostics/detail.headers/ranges/lazy_split_view.module.verify.cpp
@@ -0,0 +1,15 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: modules-build
+
+// WARNING: This test was generated by 'generate_private_header_tests.py'
+// and should not be edited manually.
+
+// expected-error@*:* {{use of private header from outside its module: '__ranges/lazy_split_view.h'}}
+#include <__ranges/lazy_split_view.h>
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/adaptor.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/adaptor.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/adaptor.pass.cpp
@@ -0,0 +1,128 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// std::views::lazy_split
+
+#include <ranges>
+
+#include <array>
+#include <cassert>
+#include <concepts>
+#include <string_view>
+#include <utility>
+
+#include "test_iterators.h"
+#include "types.h"
+
+template <class View, class T>
+concept CanBePiped = requires (View&& view, T&& t) {
+  { std::forward<View>(view) | std::forward<T>(t) };
+};
+
+struct SomeView : std::ranges::view_base {
+  const std::string_view* v_;
+  constexpr SomeView(const std::string_view& v) : v_(&v) {}
+  constexpr auto begin() const { return v_->begin(); }
+  constexpr auto end() const { return v_->end(); }
+};
+
+struct NotAView { };
+
+static_assert(!std::is_invocable_v<decltype(std::views::lazy_split)>);
+static_assert(!std::is_invocable_v<decltype(std::views::lazy_split), SomeView, NotAView>);
+static_assert(!std::is_invocable_v<decltype(std::views::lazy_split), NotAView, SomeView>);
+static_assert( std::is_invocable_v<decltype(std::views::lazy_split), SomeView, SomeView>);
+
+static_assert( CanBePiped<SomeView&,    decltype(std::views::lazy_split)>);
+static_assert( CanBePiped<char(&)[10],  decltype(std::views::lazy_split)>);
+static_assert(!CanBePiped<char(&&)[10], decltype(std::views::lazy_split)>);
+static_assert(!CanBePiped<NotAView,     decltype(std::views::lazy_split)>);
+
+static_assert(std::same_as<decltype(std::views::lazy_split), decltype(std::ranges::views::lazy_split)>);
+
+constexpr bool test() {
+  constexpr std::string_view input = "abc";
+  constexpr std::string_view sep = "a";
+
+  // Test that `std::views::lazy_split` is a range adaptor.
+
+  // Test `views::lazy_split(input, sep)`.
+  {
+    SomeView view(input);
+
+    using Result = std::ranges::lazy_split_view<SomeView, std::string_view>;
+    std::same_as<Result> auto result = std::views::lazy_split(view, sep);
+    assert(result.base().begin() == input.begin());
+    assert(result.base().end() == input.end());
+  }
+
+  // Test `views::lazy_split(sep)(input)`.
+  {
+    SomeView view(input);
+
+    using Result = std::ranges::lazy_split_view<SomeView, std::string_view>;
+    std::same_as<Result> auto result = std::views::lazy_split(sep)(view);
+    assert(result.base().begin() == input.begin());
+    assert(result.base().end() == input.end());
+  }
+
+  // Test `view | views::lazy_split`.
+  {
+    SomeView view(input);
+
+    using Result = std::ranges::lazy_split_view<SomeView, std::string_view>;
+    std::same_as<Result> auto result = view | std::views::lazy_split(sep);
+    assert(result.base().begin() == input.begin());
+    assert(result.base().end() == input.end());
+  }
+
+  // Test `adaptor | views::lazy_split`.
+  {
+    SomeView view(input);
+    auto f = [](char c) { return c; };
+    auto partial = std::views::transform(f) | std::views::lazy_split(sep);
+
+    using Result = std::ranges::lazy_split_view<std::ranges::transform_view<SomeView, decltype(f)>, std::string_view>;
+    std::same_as<Result> auto result = partial(view);
+    assert(result.base().base().begin() == input.begin());
+    assert(result.base().base().end() == input.end());
+  }
+
+  // Test `views::lazy_split | adaptor`.
+  {
+    SomeView view(input);
+    auto f = [](auto v) { return v; };
+    auto partial = std::views::lazy_split(sep) | std::views::transform(f);
+
+    using Result = std::ranges::transform_view<std::ranges::lazy_split_view<SomeView, std::string_view>, decltype(f)>;
+    std::same_as<Result> auto result = partial(view);
+    assert(result.base().base().begin() == input.begin());
+    assert(result.base().base().end() == input.end());
+  }
+
+  // Test that one can call std::views::filter with arbitrary stuff, as long as we
+  // don't try to actually complete the call by passing it a range.
+  //
+  // That makes no sense and we can't do anything with the result, but it's valid.
+  {
+    struct X { };
+    [[maybe_unused]] auto partial = std::views::lazy_split(X{});
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/base.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/base.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/base.pass.cpp
@@ -0,0 +1,106 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr View base() const& requires copy_constructible<_View>;
+// constexpr View base() &&;
+
+#include <ranges>
+
+#include <cassert>
+#include <string_view>
+#include <utility>
+#include "types.h"
+
+struct MoveOnlyView : std::ranges::view_base {
+  std::string_view view_;
+  constexpr MoveOnlyView() = default;
+  constexpr MoveOnlyView(const char* ptr) : view_(ptr) {}
+  constexpr MoveOnlyView(std::string_view v) : view_(v) {}
+  constexpr MoveOnlyView(MoveOnlyView&&) = default;
+  constexpr MoveOnlyView& operator=(MoveOnlyView&&) = default;
+  constexpr const char* begin() const { return view_.begin(); }
+  constexpr const char* end() const { return view_.end(); }
+  constexpr bool operator==(MoveOnlyView rhs) const { return view_ == rhs.view_; }
+};
+static_assert( std::ranges::view<MoveOnlyView>);
+static_assert( std::ranges::contiguous_range<MoveOnlyView>);
+static_assert(!std::copyable<MoveOnlyView>);
+
+template <class View>
+concept CanCallBase = requires(View v) { std::forward<View>(v).base(); };
+
+static_assert( CanCallBase<std::ranges::lazy_split_view<CopyableView, ForwardView>&>);
+static_assert( CanCallBase<std::ranges::lazy_split_view<CopyableView, ForwardView> const &>);
+static_assert( CanCallBase<std::ranges::lazy_split_view<CopyableView, ForwardView>&&>);
+static_assert( CanCallBase<std::ranges::lazy_split_view<CopyableView, ForwardView> const &&>);
+static_assert(!CanCallBase<std::ranges::lazy_split_view<MoveOnlyView, ForwardView>&>);
+static_assert(!CanCallBase<std::ranges::lazy_split_view<MoveOnlyView, ForwardView> const &>);
+static_assert( CanCallBase<std::ranges::lazy_split_view<MoveOnlyView, ForwardView>&&>);
+static_assert(!CanCallBase<std::ranges::lazy_split_view<MoveOnlyView, ForwardView> const &&>);
+
+struct View : std::ranges::view_base {
+  enum class InitializedBy {
+    Copy,
+    Move,
+    Invalid
+  };
+
+  std::string_view v_;
+  InitializedBy initialized_by = InitializedBy::Invalid;
+  constexpr View(std::string_view v) : v_(v) {}
+
+  constexpr auto begin() const { return v_.begin(); }
+  constexpr auto end() const { return v_.end(); }
+
+  constexpr View(const View& rhs) : v_(rhs.v_) { initialized_by = InitializedBy::Copy; }
+  constexpr View(View&& rhs) : v_(rhs.v_) { initialized_by = InitializedBy::Move; }
+  constexpr View& operator=(const View& rhs) = default;
+  constexpr View& operator=(View&& rhs) = default;
+  constexpr bool operator==(const View& rhs) const { return v_ == rhs.v_; }
+};
+
+constexpr bool test() {
+  // Copyable input -- both lvalue and rvalue overloads of `base` are available.
+  {
+    {
+      constexpr View str("abc def");
+      std::ranges::lazy_split_view<View, std::string_view> v(str, " ");
+
+      std::same_as<View> auto result = v.base();
+      assert(result == str);
+      assert(result.initialized_by == View::InitializedBy::Copy);
+    }
+
+    {
+      constexpr View str("abc def");
+      std::ranges::lazy_split_view<View, std::string_view> v(str, " ");
+
+      std::same_as<View> auto result = std::move(v).base();
+      assert(result == str);
+      assert(result.initialized_by == View::InitializedBy::Move);
+    }
+  }
+
+  // Move-only input -- only the rvalue overload of `base` is available.
+  {
+    std::ranges::lazy_split_view<MoveOnlyView, ForwardView> v;
+    assert(std::move(v).base() == MoveOnlyView());
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/begin.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/begin.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/begin.pass.cpp
@@ -0,0 +1,118 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr auto begin();
+// constexpr auto begin() const requires forward_range<View> && forward_range<const View>;
+
+#include <ranges>
+
+#include <cassert>
+#include <utility>
+#include "test_iterators.h"
+#include "types.h"
+
+template <class View, class Expected>
+concept OuterIterConceptSameAs =
+    std::same_as<typename std::ranges::iterator_t<View>::iterator_concept, Expected>;
+
+template <class View, class Expected>
+concept InnerIterValueTypeSameAs = requires (View v) {
+  { *(*v.begin()).begin() } -> std::same_as<Expected>;
+};
+
+template <class View>
+concept ConstBeginDisabled = !requires (const View v) {
+  { (*v.begin()) };
+};
+
+constexpr bool test() {
+  // non-const: forward_range<View> && simple-view<View> -> outer-iterator<Const = true>
+  // const: forward_range<View> && forward_range<const View> -> outer-iterator<Const = true>
+  {
+    using V = ForwardView;
+    using P = V;
+
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(std::ranges::forward_range<const V>);
+    LIBCPP_STATIC_ASSERT(std::ranges::__simple_view<V>);
+    LIBCPP_STATIC_ASSERT(std::ranges::__simple_view<P>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), const char&>);
+
+    const std::ranges::lazy_split_view<V, P> cv;
+    static_assert(OuterIterConceptSameAs<decltype(cv), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(cv), const char&>);
+  }
+
+  // non-const: forward_range<View> && !simple-view<View> -> outer-iterator<Const = false>
+  // const: forward_range<View> && forward_range<const View> -> outer-iterator<Const = true>
+  {
+    using V = ForwardDiffView;
+    using P = V;
+
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(std::ranges::forward_range<const V>);
+    LIBCPP_STATIC_ASSERT(!std::ranges::__simple_view<V>);
+    LIBCPP_STATIC_ASSERT(!std::ranges::__simple_view<P>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), char&>);
+
+    const std::ranges::lazy_split_view<V, P> cv;
+    static_assert(OuterIterConceptSameAs<decltype(cv), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(cv), const char&>);
+  }
+
+  // non-const: forward_range<View> && !simple-view<View> -> outer-iterator<Const = false>
+  // const: forward_range<View> && !forward_range<const View> -> disabled
+  {
+    using V = ForwardOnlyIfNonConstView;
+    using P = V;
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(!std::ranges::forward_range<const V>);
+    LIBCPP_STATIC_ASSERT(!std::ranges::__simple_view<V>);
+    LIBCPP_STATIC_ASSERT(!std::ranges::__simple_view<P>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), const char&>);
+
+    static_assert(ConstBeginDisabled<decltype(v)>);
+  }
+
+  // non-const: !forward_range<View> && tiny-range<Pattern> -> outer-iterator<Const = false>
+  // const: !forward_range<View> -> disabled
+  {
+    using V = InputView;
+    using P = ForwardTinyView;
+
+    static_assert(!std::ranges::forward_range<V>);
+    static_assert(std::ranges::forward_range<P>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::input_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), char&>);
+
+    static_assert(ConstBeginDisabled<decltype(v)>);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/constraints.compile.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/constraints.compile.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/constraints.compile.pass.cpp
@@ -0,0 +1,164 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// template<input_range V, forward_range Pattern>
+//   requires view<V> && view<Pattern> &&
+//            indirectly_comparable<iterator_t<V>, iterator_t<Pattern>, ranges::equal_to> &&
+//            (forward_range<V> || tiny-range<Pattern>)
+// class lazy_split_view;
+
+#include <ranges>
+
+#include "test_iterators.h"
+#include "types.h"
+
+struct ForwardRange {
+  forward_iterator<int*> begin() const;
+  forward_iterator<int*> end() const;
+};
+static_assert( std::ranges::forward_range<ForwardRange>);
+
+template <class View, class Pattern>
+concept CanInstantiate = requires {
+  typename std::ranges::lazy_split_view<View, Pattern>;
+};
+
+// All constraints satisfied (`View` and `Pattern` are forward views).
+namespace test1 {
+
+  using View = ForwardView;
+  using Pattern = ForwardView;
+  static_assert( std::ranges::forward_range<View>);
+  static_assert( std::ranges::forward_range<Pattern>);
+  static_assert( std::ranges::view<View>);
+  static_assert( std::ranges::view<Pattern>);
+  static_assert( std::indirectly_comparable<
+      std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+  static_assert( CanInstantiate<View, Pattern>);
+
+} // namespace test1
+
+// All constraints satisfied (`View` is an input view and `Pattern` is a tiny view).
+namespace test2 {
+
+  using View = InputView;
+  using Pattern = ForwardTinyView;
+  static_assert( std::ranges::input_range<View>);
+  static_assert( std::ranges::forward_range<Pattern>);
+  static_assert( std::ranges::view<View>);
+  static_assert( std::ranges::view<Pattern>);
+  static_assert( std::indirectly_comparable<
+      std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+  static_assert( CanInstantiate<View, Pattern>);
+
+} // namespace test2
+
+// `View` is not an input range.
+namespace test3 {
+
+  struct AlmostInputIterator {
+    using value_type = char;
+    using difference_type = ptrdiff_t;
+    using iterator_concept = int;
+
+    constexpr const char& operator*() const;
+    constexpr AlmostInputIterator& operator++();
+    constexpr void operator++(int);
+    constexpr bool operator==(const AlmostInputIterator&) const;
+  };
+
+  static_assert( std::input_or_output_iterator<AlmostInputIterator>);
+  static_assert(!std::input_iterator<AlmostInputIterator>);
+
+  struct NonInputView : std::ranges::view_base {
+    AlmostInputIterator begin() const;
+    AlmostInputIterator end() const;
+  };
+
+  using View = NonInputView;
+  using Pattern = ForwardTinyView;
+  static_assert(!std::ranges::input_range<View>);
+  static_assert( std::ranges::forward_range<Pattern>);
+  static_assert( std::ranges::view<View>);
+  static_assert( std::ranges::view<Pattern>);
+  static_assert( std::indirectly_comparable<
+      std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+  static_assert(!CanInstantiate<View, Pattern>);
+
+} // namespace test3
+
+// `View` is not a view.
+namespace test4 {
+
+  using View = ForwardRange;
+  using Pattern = ForwardView;
+  static_assert( std::ranges::input_range<View>);
+  static_assert( std::ranges::forward_range<Pattern>);
+  static_assert(!std::ranges::view<View>);
+  static_assert( std::ranges::view<Pattern>);
+  static_assert( std::indirectly_comparable<
+      std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+  static_assert(!CanInstantiate<View, Pattern>);
+
+} // namespace test4
+
+// `Pattern` is not a forward range.
+namespace test5 {
+
+  using View = ForwardView;
+  using Pattern = InputView;
+  static_assert( std::ranges::input_range<View>);
+  static_assert(!std::ranges::forward_range<Pattern>);
+  static_assert( std::ranges::view<View>);
+  static_assert( std::ranges::view<Pattern>);
+  static_assert( std::indirectly_comparable<
+      std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+  static_assert(!CanInstantiate<View, Pattern>);
+
+} // namespace test5
+
+// Not indirectly comparable.
+namespace test6 {
+
+  struct Empty{};
+  struct IntForwardView : std::ranges::view_base {
+    constexpr forward_iterator<Empty*> begin() const { return {}; }
+    constexpr forward_iterator<Empty*> end() const { return {}; }
+  };
+
+  using View = ForwardView;
+  using Pattern = IntForwardView;
+  static_assert( std::ranges::input_range<View>);
+  static_assert( std::ranges::forward_range<Pattern>);
+  static_assert( std::ranges::view<View>);
+  static_assert( std::ranges::view<Pattern>);
+  static_assert(!std::indirectly_comparable<
+      std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+  static_assert(!CanInstantiate<View, Pattern>);
+
+} // namespace test6
+
+// `View` is not a forward range and `Pattern` is not a tiny range.
+namespace test7 {
+
+    using View = InputView;
+    using Pattern = ForwardView;
+    static_assert( std::ranges::input_range<View>);
+    static_assert(!std::ranges::forward_range<View>);
+    static_assert( std::ranges::forward_range<Pattern>);
+    LIBCPP_STATIC_ASSERT(!std::ranges::__tiny_range<Pattern>);
+    static_assert( std::ranges::view<View>);
+    static_assert( std::ranges::view<Pattern>);
+    static_assert( std::indirectly_comparable<
+        std::ranges::iterator_t<View>, std::ranges::iterator_t<Pattern>, std::ranges::equal_to>);
+    static_assert(!CanInstantiate<View, Pattern>);
+
+} // namespace test7
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/copy.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/copy.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/copy.pass.cpp
@@ -0,0 +1,90 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// class std::ranges::lazy_split_view;
+
+#include <ranges>
+
+#include <cassert>
+#include <string_view>
+#include <utility>
+#include "small_string.h"
+#include "types.h"
+
+constexpr bool operator==(const InputView& lhs, const InputView& rhs) {
+  return SmallString(lhs) == SmallString(rhs);
+}
+
+constexpr bool test() {
+  // Can copy `lazy_split_view`.
+  {
+    // Forward range.
+    {
+      std::ranges::lazy_split_view<std::string_view, std::string_view> v1("abc def", " ");
+      auto v2 = v1;
+      assert(v2.base() == v1.base());
+    }
+
+    // Input range.
+    {
+      SplitViewInput v1("abc def", ' ');
+      auto v2 = v1;
+      assert(v2.base() == v1.base());
+    }
+  }
+
+  // Can move `lazy_split_view`.
+  {
+    // Forward range.
+    {
+      std::string_view base = "abc def";
+      std::ranges::lazy_split_view<std::string_view, std::string_view> v1(base, " ");
+      auto v2 = std::move(v1);
+      assert(v2.base() == base);
+    }
+
+    // Input range.
+    {
+      InputView base("abc def");
+      SplitViewInput v1(base, ' ');
+      auto v2 = std::move(v1);
+      assert(v2.base() == base);
+    }
+  }
+
+  // `non-propagating-cache` is not copied.
+  {
+    SplitViewInput v1("abc def ghi", ' ');
+    auto outer_iter1 = v1.begin();
+    ++outer_iter1;
+    auto val1 = *outer_iter1;
+    auto i1 = val1.begin();
+    assert(*i1 == 'd');
+    ++i1;
+    assert(*i1 == 'e');
+
+    auto v2 = v1;
+    auto val2 = *v2.begin();
+    auto i2 = val2.begin();
+    assert(*i2 == 'a');
+    ++i2;
+    assert(*i2 == 'b');
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctad.compile.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctad.compile.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctad.compile.pass.cpp
@@ -0,0 +1,65 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// template <class R, class P>
+// lazy_split_view(R&&, P&&) -> lazy_split_view<views::all_t<R>, views::all_t<P>>;
+//
+// template <input_range R>
+// lazy_split_view(R&&, range_value_t<R>) -> lazy_split_view<views::all_t<R>, single_view<range_value_t<R>>>;
+
+#include <ranges>
+
+#include <concepts>
+#include <type_traits>
+#include <utility>
+#include "types.h"
+
+struct ForwardRange {
+  forward_iterator<int*> begin() const;
+  forward_iterator<int*> end() const;
+};
+static_assert( std::ranges::forward_range<ForwardRange>);
+
+struct InputRange {
+  cpp20_input_iterator<const char*> begin() const;
+  std::default_sentinel_t end() const;
+};
+bool operator==(const cpp20_input_iterator<const char*>&, std::default_sentinel_t);
+static_assert(std::ranges::input_range<InputRange>);
+
+template <class I1, class I2, class Expected>
+constexpr void test() {
+  I1 i1{};
+  I2 i2{};
+
+  std::ranges::lazy_split_view v(std::move(i1), std::move(i2));
+  using O = decltype(std::move(v).base());
+  static_assert(std::same_as<O, Expected>);
+}
+
+constexpr void testCtad() {
+  // (Range, Pattern)
+  test<ForwardView, ForwardView, ForwardView>();
+  test<ForwardRange, ForwardRange, std::ranges::views::all_t<ForwardRange>>();
+
+  // (Range, RangeElement)
+  test<ForwardRange, char, std::ranges::views::all_t<ForwardRange>>();
+  test<InputRange, char, std::ranges::views::all_t<InputRange>>();
+
+  // (Range, RangeElement) with implicit conversion.
+  test<ForwardRange, bool, std::ranges::views::all_t<ForwardRange>>();
+  test<InputRange, bool, std::ranges::views::all_t<InputRange>>();
+
+  // Note: CTAD from (InputRange, TinyForwardRange) doesn't work -- the deduction guide wraps the pattern in
+  // `views::all_t`, resulting in `views::owning_view<TinyForwardRange>`. That type would never satisfy `tiny-range`
+  // because `views::owning_view` contains a member function `size()` that shadows the static `size()` in
+  // `TinyForwardRange`.
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.default.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.default.pass.cpp
@@ -0,0 +1,59 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+//  lazy_split_view() requires default_initializable<V> && default_initializable<P> = default;
+
+#include <ranges>
+
+#include <cassert>
+#include "types.h"
+
+struct ThrowingDefaultCtorForwardView : std::ranges::view_base {
+  ThrowingDefaultCtorForwardView() noexcept(false);
+  forward_iterator<int*> begin() const;
+  forward_iterator<int*> end() const;
+};
+
+struct NoDefaultCtorForwardView : std::ranges::view_base {
+  NoDefaultCtorForwardView() = delete;
+  forward_iterator<int*> begin() const;
+  forward_iterator<int*> end() const;
+};
+
+static_assert( std::is_default_constructible_v<std::ranges::lazy_split_view<ForwardView, ForwardView>>);
+static_assert(!std::is_default_constructible_v<std::ranges::lazy_split_view<NoDefaultCtorForwardView, ForwardView>>);
+static_assert(!std::is_default_constructible_v<std::ranges::lazy_split_view<ForwardView, NoDefaultCtorForwardView>>);
+
+static_assert( std::is_nothrow_default_constructible_v<std::ranges::lazy_split_view<ForwardView, ForwardView>>);
+static_assert(!std::is_nothrow_default_constructible_v<ThrowingDefaultCtorForwardView>);
+static_assert(!std::is_nothrow_default_constructible_v<
+    std::ranges::lazy_split_view<ThrowingDefaultCtorForwardView, ForwardView>>);
+
+constexpr bool test() {
+  {
+    std::ranges::lazy_split_view<CopyableView, ForwardView> v;
+    assert(v.base() == CopyableView());
+  }
+
+  {
+    std::ranges::lazy_split_view<CopyableView, ForwardView> v = {};
+    assert(v.base() == CopyableView());
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.range.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.range.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.range.pass.cpp
@@ -0,0 +1,47 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// template <input_range Range>
+//   requires constructible_from<View, views::all_t<Range>> &&
+//             constructible_from<Pattern, single_view<range_value_t<Range>>>
+// constexpr lazy_split_view(Range&& r, range_value_t<Range> e);
+
+#include <ranges>
+
+#include <cassert>
+#include <string_view>
+#include <type_traits>
+#include <utility>
+#include "types.h"
+
+constexpr bool test() {
+  decltype(auto) str = "abc def";
+  std::ranges::lazy_split_view<std::string_view, std::string_view> v(str, ' ');
+  assert(v.base() == std::string_view(str, sizeof(str)));
+
+  {
+    using V = std::ranges::lazy_split_view<std::string_view, std::string_view>;
+    static_assert( std::is_constructible_v<V, std::string_view, char>);
+    static_assert( std::is_constructible_v<V, char(&)[5], char>);
+    struct Empty {};
+    static_assert(!std::is_constructible_v<V, Empty, std::string_view>);
+    static_assert(!std::is_constructible_v<V, std::string_view, Empty>);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.view.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.view.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/ctor.view.pass.cpp
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr lazy_split_view(View base, Pattern pattern);
+
+#include <ranges>
+
+#include <cassert>
+#include <string_view>
+
+constexpr bool test() {
+  constexpr std::string_view str = "abc def";
+  std::ranges::lazy_split_view<std::string_view, std::string_view> v(str, " ");
+  assert(v.base() == str);
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/end.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/end.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/end.pass.cpp
@@ -0,0 +1,152 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr auto end() requires forward_range<View> && common_range<View>;
+// constexpr auto end() const;
+
+#include <ranges>
+
+#include <cassert>
+#include <utility>
+#include "test_iterators.h"
+#include "types.h"
+
+template <class View, class Expected>
+concept OuterIterConceptSameAs =
+    std::same_as<typename decltype(std::declval<View>().end())::iterator_concept, Expected>;
+
+template <class View, class Expected>
+concept InnerIterValueTypeSameAs = requires (View v) {
+  { *(*v.end()).begin() } -> std::same_as<Expected>;
+};
+
+struct ForwardViewCommonIfConst : std::ranges::view_base {
+  std::string_view view_;
+  constexpr explicit ForwardViewCommonIfConst() = default;
+  constexpr ForwardViewCommonIfConst(const char* ptr) : view_(ptr) {}
+  constexpr ForwardViewCommonIfConst(std::string_view v) : view_(v) {}
+  constexpr ForwardViewCommonIfConst(ForwardViewCommonIfConst&&) = default;
+  constexpr ForwardViewCommonIfConst& operator=(ForwardViewCommonIfConst&&) = default;
+  constexpr ForwardViewCommonIfConst(const ForwardViewCommonIfConst&) = default;
+  constexpr ForwardViewCommonIfConst& operator=(const ForwardViewCommonIfConst&) = default;
+  constexpr forward_iterator<char*> begin() { return forward_iterator<char*>(nullptr); }
+  constexpr std::default_sentinel_t end()  { return std::default_sentinel; }
+  constexpr forward_iterator<const char*> begin() const { return forward_iterator<const char*>(view_.begin()); }
+  constexpr forward_iterator<const char*> end() const { return forward_iterator<const char*>(view_.end()); }
+};
+bool operator==(forward_iterator<char*>, std::default_sentinel_t) { return false; }
+
+struct ForwardViewNonCommonRange : std::ranges::view_base {
+  std::string_view view_;
+  constexpr explicit ForwardViewNonCommonRange() = default;
+  constexpr ForwardViewNonCommonRange(const char* ptr) : view_(ptr) {}
+  constexpr ForwardViewNonCommonRange(std::string_view v) : view_(v) {}
+  constexpr ForwardViewNonCommonRange(ForwardViewNonCommonRange&&) = default;
+  constexpr ForwardViewNonCommonRange& operator=(ForwardViewNonCommonRange&&) = default;
+  constexpr ForwardViewNonCommonRange(const ForwardViewNonCommonRange&) = default;
+  constexpr ForwardViewNonCommonRange& operator=(const ForwardViewNonCommonRange&) = default;
+  constexpr forward_iterator<char*> begin() { return forward_iterator<char*>(nullptr); }
+  constexpr std::default_sentinel_t end()  { return std::default_sentinel; }
+  constexpr forward_iterator<const char*> begin() const { return forward_iterator<const char*>(view_.begin()); }
+  constexpr std::default_sentinel_t end() const { return std::default_sentinel; }
+};
+bool operator==(forward_iterator<const char*>, std::default_sentinel_t) { return false; }
+
+constexpr bool test() {
+  // non-const: forward_range<V> && simple_view<V> && simple_view<P> -> outer-iterator<Const = true>
+  // const: forward_range<V> && common_range<V> -> outer-iterator<Const = true>
+  {
+    using V = ForwardView;
+    using P = V;
+
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(std::ranges::common_range<const V>);
+    LIBCPP_STATIC_ASSERT(std::ranges::__simple_view<V>);
+    LIBCPP_STATIC_ASSERT(std::ranges::__simple_view<P>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), const char&>);
+
+    const std::ranges::lazy_split_view<V, P> cv;
+    static_assert(OuterIterConceptSameAs<decltype(cv), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(cv), const char&>);
+  }
+
+  // non-const: forward_range<V> && common_range<V> && simple_view<V> && !simple_view<P> -> outer-iterator<Const=false>
+  // const: forward_range<V> && forward_range<const V> && common_range<const V> -> outer-iterator<Const = false>
+  {
+    using V = ForwardView;
+    using P = ForwardDiffView;
+
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(std::ranges::common_range<V>);
+    LIBCPP_STATIC_ASSERT(std::ranges::__simple_view<V>);
+    LIBCPP_STATIC_ASSERT(!std::ranges::__simple_view<P>);
+    static_assert(std::ranges::forward_range<const V>);
+    static_assert(std::ranges::common_range<const V>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), const char&>);
+
+    const std::ranges::lazy_split_view<V, P> cv;
+    static_assert(OuterIterConceptSameAs<decltype(cv), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(cv), const char&>);
+  }
+
+  // non-const: forward_range<V> && !common_range<V> -> disabled
+  // const: forward_range<V> && forward_range<const V> && common_range<const V> -> outer-iterator<Const = true>
+  {
+    using V = ForwardViewCommonIfConst;
+    using P = V;
+
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(!std::ranges::common_range<V>);
+    static_assert(std::ranges::forward_range<const V>);
+    static_assert(std::ranges::common_range<const V>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(OuterIterConceptSameAs<decltype(v), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(v), const char&>);
+
+    const std::ranges::lazy_split_view<V, P> cv;
+    static_assert(OuterIterConceptSameAs<decltype(cv), std::forward_iterator_tag>);
+    static_assert(InnerIterValueTypeSameAs<decltype(cv), const char&>);
+  }
+
+  // non-const: forward_range<V> && !common_range<V> -> disabled
+  // const: forward_range<V> && forward_range<const V> && !common_range<const V> -> outer-iterator<Const = false>
+  {
+    using V = ForwardViewNonCommonRange;
+    using P = V;
+
+    static_assert(std::ranges::forward_range<V>);
+    static_assert(!std::ranges::common_range<V>);
+    static_assert(std::ranges::forward_range<const V>);
+    static_assert(!std::ranges::common_range<const V>);
+
+    std::ranges::lazy_split_view<V, P> v;
+    static_assert(std::same_as<decltype(v.end()), std::default_sentinel_t>);
+
+    const std::ranges::lazy_split_view<V, P> cv;
+    static_assert(std::same_as<decltype(cv.end()), std::default_sentinel_t>);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/general.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/general.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/general.pass.cpp
@@ -0,0 +1,404 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// class std::ranges::lazy_split_view;
+
+#include <ranges>
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <string_view>
+#include <utility>
+#include "small_string.h"
+
+#include <string>
+#include <iostream>
+template <class T, class Separator, class U, size_t M>
+constexpr bool test_function_call(T&& input, Separator separator, std::array<U, M> expected) {
+  std::ranges::lazy_split_view v(input, separator);
+
+  return std::equal(v.begin(), v.end(), expected.begin(), expected.end(),
+      [](const auto& split_value, const auto& expected_value) {
+        return SmallString(split_value) == expected_value;
+  });
+}
+
+template <class T, class Separator, class U, size_t M>
+constexpr bool test_with_piping(T&& input, Separator separator, std::array<U, M> expected) {
+  auto expected_it = expected.begin();
+  for (auto e : input | std::ranges::views::lazy_split(separator)) {
+    if (expected_it == expected.end()) return false;
+
+    if (SmallString(e) != *expected_it) return false;
+    ++expected_it;
+  }
+
+  return expected_it == expected.end();
+}
+
+constexpr bool test_l_r_values() {
+  using namespace std::string_view_literals;
+
+  // Both lvalues and rvalues can be used as input.
+  {
+    // Lvalues.
+    {
+      auto input = "abc"sv;
+      auto sep = " "sv;
+      [[maybe_unused]] std::ranges::lazy_split_view v(input, sep);
+    }
+
+    // Const lvalues.
+    {
+      const auto input = "abc"sv;
+      const auto sep = " "sv;
+      [[maybe_unused]] std::ranges::lazy_split_view v(input, sep);
+    }
+
+    // Rvalues.
+    {
+      [[maybe_unused]] std::ranges::lazy_split_view v("abc"sv, " "sv);
+    }
+
+    // Const rvalues.
+    {
+      const auto input = "abc"sv;
+      const auto sep = " "sv;
+      [[maybe_unused]] std::ranges::lazy_split_view v(std::move(input), std::move(sep));
+    }
+  }
+
+  return true;
+}
+
+constexpr bool test() {
+  using namespace std::string_view_literals;
+
+  constexpr char short_sep = ' ';
+  constexpr auto long_sep = "12"sv;
+  // Make sure that a string literal and a `string_view` produce the same results (which isn't always the case, see
+  // below).
+  constexpr auto sv = [](auto&& str) { return std::string_view(str); };
+
+  // One separator.
+  {
+    {
+      decltype(auto) input = "abc def";
+      constexpr std::array expected = {"abc"sv, "def"sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "abc12def";
+      constexpr std::array expected = {"abc"sv, "def"sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Several separators in a row.
+  {
+    {
+      decltype(auto) input = "abc    def";
+      constexpr std::array expected = {"abc"sv, ""sv, ""sv, ""sv, "def"sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "abc12121212def";
+      constexpr std::array expected = {"abc"sv, ""sv, ""sv, ""sv, "def"sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Trailing separator.
+  {
+    {
+      decltype(auto) input = "abc def ";
+      constexpr std::array expected = {"abc"sv, "def"sv, ""sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "abc12def12";
+      constexpr std::array expected = {"abc"sv, "def"sv, ""sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Leading separator.
+  {
+    {
+      decltype(auto) input = " abc def";
+      constexpr std::array expected = {""sv, "abc"sv, "def"sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "12abc12def";
+      constexpr std::array expected = {""sv, "abc"sv, "def"sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // No separator.
+  {
+    {
+      decltype(auto) input = "abc";
+      constexpr std::array expected = {"abc"sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "abc";
+      constexpr std::array expected = {"abc"sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Empty string.
+  {
+    // Note: this is one of the edge cases where a string literal and a `string_view` produce different results.
+    // This is essentially because `size(""sv)` is `0` whereas `size("")` is `1` (accounting for the terminating null).
+    // Thus, the terminating zero is ignored for a `string_view` but treated as a separate character for a string
+    // literal.
+    {
+      decltype(auto) input = "";
+      constexpr std::array expected_literal = {""sv};
+      constexpr std::array<std::string_view, 0> expected_sv = {};
+
+      assert(test_function_call(input, short_sep, expected_literal));
+      assert(test_function_call(sv(input), short_sep, expected_sv));
+      assert(test_with_piping(input, short_sep, expected_literal));
+      assert(test_with_piping(sv(input), short_sep, expected_sv));
+    }
+
+    {
+      decltype(auto) input = "";
+      constexpr std::array expected_literal = {""sv};
+      constexpr std::array<std::string_view, 0> expected_sv = {};
+
+      assert(test_function_call(input, long_sep, expected_literal));
+      assert(test_function_call(sv(input), long_sep, expected_sv));
+      assert(test_with_piping(input, long_sep, expected_literal));
+      assert(test_with_piping(sv(input), long_sep, expected_sv));
+    }
+  }
+
+  // Input consisting of a single separator.
+  {
+    {
+      decltype(auto) input = " ";
+      constexpr std::array expected = {""sv, ""sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "12";
+      constexpr std::array expected = {""sv, ""sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Input consisting of only separators.
+  {
+    {
+      decltype(auto) input = "   ";
+      constexpr std::array expected = {""sv, ""sv, ""sv, ""sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "121212";
+      constexpr std::array expected = {""sv, ""sv, ""sv, ""sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Many redundant separators.
+  {
+    {
+      decltype(auto) input = "  abc   def  ";
+      constexpr std::array expected = {""sv, ""sv, "abc"sv, ""sv, ""sv, "def"sv, ""sv, ""sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "1212abc121212def1212";
+      constexpr std::array expected = {""sv, ""sv, "abc"sv, ""sv, ""sv, "def"sv, ""sv, ""sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Separators after every character.
+  {
+    {
+      decltype(auto) input = " a b c ";
+      constexpr std::array expected = {""sv, "a"sv, "b"sv, "c"sv, ""sv};
+
+      assert(test_function_call(input, short_sep, expected));
+      assert(test_function_call(sv(input), short_sep, expected));
+      assert(test_with_piping(input, short_sep, expected));
+      assert(test_with_piping(sv(input), short_sep, expected));
+    }
+
+    {
+      decltype(auto) input = "12a12b12c12";
+      constexpr std::array expected = {""sv, "a"sv, "b"sv, "c"sv, ""sv};
+
+      assert(test_function_call(input, long_sep, expected));
+      assert(test_function_call(sv(input), long_sep, expected));
+      assert(test_with_piping(input, long_sep, expected));
+      assert(test_with_piping(sv(input), long_sep, expected));
+    }
+  }
+
+  // Empty separator.
+  {
+    // Note: this is one of the edge cases where a string literal and a `string_view` produce different results.
+    // This is essentially because `size(""sv)` is `0` whereas `size("")` is `1` (accounting for the terminating null).
+    // Thus, the terminating zero is ignored for a `string_view` but treated as a separate character for a string
+    // literal.
+    {
+      constexpr auto empty_sep = ""sv;
+
+      decltype(auto) input = "abc";
+      constexpr std::array expected_literal = {"a"sv, "b"sv, "c"sv, ""sv};
+      constexpr std::array expected_sv = {"a"sv, "b"sv, "c"sv};
+
+      assert(test_function_call(input, empty_sep, expected_literal));
+      assert(test_function_call(sv(input), empty_sep, expected_sv));
+      assert(test_with_piping(input, empty_sep, expected_literal));
+      assert(test_with_piping(sv(input), empty_sep, expected_sv));
+    }
+  }
+
+  // Overlap between the separator and the string (see https://wg21.link/lwg3505).
+  {
+    {
+      constexpr auto overlapping_sep = "ab"sv;
+
+      decltype(auto) input = "aabaaababb";
+      constexpr std::array expected = {"a"sv, "aa"sv, ""sv, "b"sv};
+
+      assert(test_function_call(input, overlapping_sep, expected));
+      assert(test_function_call(sv(input), overlapping_sep, expected));
+      assert(test_with_piping(input, overlapping_sep, expected));
+      assert(test_with_piping(sv(input), overlapping_sep, expected));
+    }
+  }
+
+  // Non-character input.
+  {
+    {
+      constexpr int sep = 0;
+      constexpr int input[] = {1, 2, 3, 0, 4, 5, 6};
+      constexpr std::array expected = {std::array{1, 2, 3}, std::array{4, 5, 6}};
+
+      assert(test_function_call(input, sep, expected));
+      assert(test_with_piping(input, sep, expected));
+    }
+
+    {
+      constexpr std::array sep = {0, 0, 0};
+      constexpr int input[] = {1, 2, 3, 0, 0, 0, 4, 5, 6};
+      constexpr std::array expected = {std::array{1, 2, 3}, std::array{4, 5, 6}};
+
+      assert(test_function_call(input, sep, expected));
+      assert(test_with_piping(input, sep, expected));
+    }
+  }
+
+  return true;
+}
+
+template <class T>
+std::string str(const T& x) {
+  std::string result;
+  for (char c : x) {
+      result += c;
+  }
+  return result;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  test_l_r_values();
+  static_assert(test_l_r_values());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/base.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/base.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/base.pass.cpp
@@ -0,0 +1,78 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr const iterator_t<Base>& inner-iterator::base() const& noexcept;
+//
+// constexpr iterator_t<Base> inner-iterator::base() &&
+//   requires forward_range<View>;
+
+#include <ranges>
+
+#include <concepts>
+#include <utility>
+#include "../types.h"
+
+template <class InnerIter, class ReturnT>
+concept CanCallBase = requires(InnerIter i) {
+  { std::forward<InnerIter>(i).base() } -> std::same_as<ReturnT>;
+};
+
+static_assert( CanCallBase<InnerIterForward&, const BaseIterForward&>);
+static_assert( CanCallBase<InnerIterForward const &, const BaseIterForward&>);
+static_assert( CanCallBase<InnerIterForward &&, BaseIterForward>);
+static_assert( CanCallBase<InnerIterForward const &&, const BaseIterForward&>);
+
+static_assert( CanCallBase<InnerIterInput&, const BaseIterInput&>);
+static_assert( CanCallBase<InnerIterInput const &, const BaseIterInput&>);
+static_assert( CanCallBase<InnerIterInput &&, const BaseIterInput&>);
+static_assert( CanCallBase<InnerIterInput const &&, const BaseIterInput&>);
+
+static_assert( noexcept(std::declval<InnerIterForward&>().base()));
+static_assert( noexcept(std::declval<InnerIterForward const &>().base()));
+static_assert( noexcept(std::declval<InnerIterForward const &&>().base()));
+static_assert( noexcept(std::declval<InnerIterInput&>().base()));
+static_assert( noexcept(std::declval<InnerIterInput const &>().base()));
+static_assert( noexcept(std::declval<InnerIterInput const &&>().base()));
+
+constexpr bool test() {
+  // `base` works with a forward view (two different overloads based on ref-qualification of the `inner-iterator`).
+  {
+    using BaseIter = std::ranges::iterator_t<CopyableView>;
+    CopyableView b("abc def");
+    std::ranges::lazy_split_view<CopyableView, ForwardView> v(b, " ");
+
+    auto i = (*v.begin()).begin();
+    static_assert(std::same_as<decltype(i.base()), const BaseIter&>);
+    assert(i.base() == b.begin());
+    static_assert(std::same_as<decltype(std::move(i).base()), BaseIter>);
+    assert(std::move(i).base() == b.begin());
+  }
+
+  // `base` works with an input view (no overloads).
+  {
+    using BaseIter = std::ranges::iterator_t<InputView>;
+    InputView b("abc def");
+    std::ranges::lazy_split_view<InputView, ForwardTinyView> v(b, ' ');
+
+    auto i = (*v.begin()).begin();
+    static_assert(std::same_as<decltype(i.base()), const BaseIter&>);
+    static_assert(std::same_as<decltype(std::move(i).base()), const BaseIter&>);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/ctor.default.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/ctor.default.pass.cpp
@@ -0,0 +1,38 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr inner-iterator::inner-iterator() = default;
+
+#include <ranges>
+
+#include "../types.h"
+
+static_assert(std::is_default_constructible_v<InnerIterForward>);
+static_assert(std::is_default_constructible_v<InnerIterInput>);
+
+constexpr bool test() {
+  {
+    [[maybe_unused]] InnerIterForward i;
+  }
+
+  {
+    [[maybe_unused]] InnerIterInput i;
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/ctor.outer_iterator.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/ctor.outer_iterator.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/ctor.outer_iterator.pass.cpp
@@ -0,0 +1,58 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr explicit inner-iterator::inner-iterator(outer-iterator<Const> i);
+
+#include <ranges>
+
+#include "../types.h"
+
+static_assert( std::is_constructible_v<InnerIterForward, OuterIterForward>);
+static_assert( std::is_constructible_v<InnerIterInput, OuterIterInput>);
+// Is only constructible if both the outer and the inner iterators have the same constness.
+static_assert( std::is_constructible_v<InnerIterConst, OuterIterConst>);
+// Note: this works because of an implicit conversion (`OuterIterNonConst` is converted to `OuterIterConst`).
+static_assert( std::is_constructible_v<InnerIterConst, OuterIterNonConst>);
+static_assert( std::is_constructible_v<InnerIterNonConst, OuterIterNonConst>);
+static_assert(!std::is_constructible_v<InnerIterNonConst, OuterIterConst>);
+
+template <class Inner>
+void CheckCopyInitialization(Inner);
+
+template <class Inner, class Outer>
+concept CannotCopyInitialize =
+  requires(Inner i) { CheckCopyInitialization(i); } &&
+  !requires(Outer o) { CheckCopyInitialization<Inner>(o); };
+
+template <class Inner, class Outer>
+constexpr void test_impl() {
+  [[maybe_unused]] Inner i(Outer{});
+
+  // Verify that the constructor is `explicit`.
+  static_assert(CannotCopyInitialize<Inner, Outer>);
+}
+
+constexpr bool test() {
+  test_impl<InnerIterForward, OuterIterForward>();
+  test_impl<InnerIterInput, OuterIterInput>();
+  test_impl<InnerIterConst, OuterIterConst>();
+  test_impl<InnerIterConst, OuterIterNonConst>();
+  test_impl<InnerIterNonConst, OuterIterNonConst>();
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/deref.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/deref.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/deref.pass.cpp
@@ -0,0 +1,74 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr decltype(auto) inner-iterator::operator*() const;
+
+#include <ranges>
+
+#include "../types.h"
+
+constexpr bool test() {
+  // Can call `inner-iterator::operator*`; `View` is a forward range.
+  {
+    SplitViewDiff v("abc def", " ");
+    auto val = *v.begin();
+
+    // Non-const iterator.
+    {
+      auto i = val.begin();
+      static_assert(std::same_as<decltype(*i), char&>);
+      assert(*i == 'a');
+      assert(*(++i) == 'b');
+      assert(*(++i) == 'c');
+    }
+
+    // Const iterator.
+    {
+      const auto ci = val.begin();
+      static_assert(std::same_as<decltype(*ci), char&>);
+      assert(*ci == 'a');
+    }
+  }
+
+  // Can call `inner-iterator::operator*`; `View` is an input range.
+  {
+    SplitViewInput v("abc def", ' ');
+    auto val = *v.begin();
+
+    // Non-const iterator.
+    {
+      auto i = val.begin();
+      static_assert(std::same_as<decltype(*i), char&>);
+      assert(*i == 'a');
+      assert(*(++i) == 'b');
+      assert(*(++i) == 'c');
+    }
+
+    // Const iterator.
+    {
+      const auto ci = val.begin();
+      static_assert(std::same_as<decltype(*ci), char&>);
+      // Note: this can be surprising. When the underlying range is an input range, `current` is stored in the
+      // `lazy_split_view` itself and shared between `inner-iterator`s. Consequently, incrementing one iterator
+      // effectively increments all of them.
+      assert(*ci == 'c');
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/equal.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/equal.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/equal.pass.cpp
@@ -0,0 +1,76 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// friend constexpr bool operator==(const inner-iterator& x, const inner-iterator& y);
+//   requires forward_range<Base>;
+//
+// friend constexpr bool operator==(const inner-iterator& x, default_sentinel_t);
+
+#include <ranges>
+
+#include <concepts>
+#include <string_view>
+#include "../types.h"
+
+template <class Iter>
+concept CanCallEquals = requires(const Iter& i) {
+  i == i;
+  i != i;
+};
+
+constexpr bool test() {
+  // When `View` is a forward range, `inner-iterator` supports both overloads of `operator==`.
+  {
+    SplitViewForward v("abc def", " ");
+    auto val = *v.begin();
+    auto b = val.begin();
+    std::same_as<std::default_sentinel_t> auto e = val.end();
+
+    // inner-iterator == inner-iterator
+    {
+      assert(b == b);
+      assert(!(b != b));
+    }
+
+    // inner-iterator == default_sentinel
+    {
+      assert(!(b == e));
+      assert(b != e);
+
+      assert(!(b == std::default_sentinel));
+      assert(b != std::default_sentinel);
+    }
+  }
+
+  // When `View` is an input range, `inner-iterator only supports comparing an `inner-iterator` to the default sentinel.
+  {
+    SplitViewInput v("abc def", ' ');
+    auto val = *v.begin();
+    auto b = val.begin();
+    std::same_as<std::default_sentinel_t> auto e = val.end();
+
+    static_assert(!CanCallEquals<decltype(b)>);
+
+    assert(!(b == std::default_sentinel));
+    assert(b != std::default_sentinel);
+    assert(!(b == e));
+    assert(b != e);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/increment.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/increment.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/increment.pass.cpp
@@ -0,0 +1,127 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr inner-iterator& inner-iterator::operator++();
+//
+// constexpr decltype(auto) inner-iterator::operator++(int);
+
+#include <ranges>
+
+#include "../types.h"
+
+struct EmptyView : std::ranges::view_base {
+  constexpr int* begin() const { return nullptr; }
+  constexpr int* end() const { return nullptr; }
+  constexpr static size_t size() { return 0; }
+};
+static_assert(std::ranges::forward_range<EmptyView>);
+static_assert(std::ranges::view<EmptyView>);
+LIBCPP_STATIC_ASSERT(std::ranges::__tiny_range<EmptyView>);
+
+constexpr bool test() {
+  // Can call `inner-iterator::operator++`; `View` is a forward range.
+  {
+    SplitViewForward v("abc def", " ");
+    auto val = *v.begin();
+
+    // ++i
+    {
+      auto i = val.begin();
+      assert(*i == 'a');
+
+      decltype(auto) i2 = ++i;
+      static_assert(std::is_lvalue_reference_v<decltype(i2)>);
+      assert(*i2 == 'b');
+    }
+
+    // i++
+    {
+      auto i = val.begin();
+      assert(*i == 'a');
+
+      decltype(auto) i2 = i++;
+      static_assert(!std::is_reference_v<decltype(i2)>);
+      assert(*i2 == 'a');
+      assert(*i == 'b');
+    }
+  }
+
+  // Can call `inner-iterator::operator++`; `View` is an input range.
+  {
+    // ++i
+    {
+      SplitViewInput v("abc def", ' ');
+      auto val = *v.begin();
+
+      auto i = val.begin();
+      assert(*i == 'a');
+
+      decltype(auto) i2 = ++i;
+      static_assert(std::is_lvalue_reference_v<decltype(i2)>);
+      assert(*i2 == 'b');
+    }
+
+    // i++
+    {
+      SplitViewInput v("abc def", ' ');
+      auto val = *v.begin();
+
+      auto i = val.begin();
+      assert(*i == 'a');
+
+      static_assert(std::is_void_v<decltype(i++)>);
+      i++;
+      assert(*i == 'b');
+    }
+  }
+
+  // Can call `inner-iterator::operator++`; `View` is an input range and `Pattern` is an "empty" range.
+  {
+    // ++i
+    {
+      std::ranges::lazy_split_view<InputView, EmptyView> v("a", EmptyView());
+      auto val = *v.begin();
+
+      auto i = val.begin();
+      assert(*i.base() == 'a');
+      assert(i != std::default_sentinel);
+
+      // The iterator doesn't move to the next character but is considered to point to the end.
+      ++i;
+      assert(*i.base() == 'a');
+      assert(i == std::default_sentinel);
+    }
+
+    // i++
+    {
+      std::ranges::lazy_split_view<InputView, EmptyView> v("a", EmptyView());
+      auto val = *v.begin();
+
+      auto i = val.begin();
+      assert(*i.base() == 'a');
+      assert(i != std::default_sentinel);
+
+      // The iterator doesn't move to the next character but is considered to point to the end.
+      i++;
+      assert(*i.base() == 'a');
+      assert(i == std::default_sentinel);
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/iter_move.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/iter_move.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/iter_move.pass.cpp
@@ -0,0 +1,158 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// friend constexpr decltype(auto) iter_move(const inner-iterator& i)
+//   noexcept(noexcept(ranges::iter_move(i.i_.<current>)));
+
+#include <iterator>
+
+#include <cassert>
+#include <type_traits>
+#include <utility>
+#include "../types.h"
+
+namespace adl {
+
+template <bool IsNoexcept = false>
+struct Iterator {
+  using value_type = int;
+  using difference_type = ptrdiff_t;
+
+  value_type* ptr_ = nullptr;
+  int* iter_move_invocations_ = nullptr;
+
+  constexpr Iterator() = default;
+  constexpr explicit Iterator(int* p, int& iter_moves) : ptr_(p), iter_move_invocations_(&iter_moves) {}
+
+  constexpr value_type& operator*() const { return *ptr_; }
+
+  Iterator& operator++() { ++ptr_; return *this; }
+  Iterator operator++(int) {
+    Iterator prev = *this;
+    ++ptr_;
+    return prev;
+  }
+
+  constexpr Iterator& operator--() { --ptr_; return *this; }
+  constexpr Iterator operator--(int) {
+    Iterator prev = *this;
+    --ptr_;
+    return prev;
+  }
+
+  constexpr friend value_type&& iter_move(Iterator iter) noexcept(IsNoexcept) {
+    if (iter.iter_move_invocations_) {
+      ++(*iter.iter_move_invocations_);
+    }
+    return std::move(*iter);
+  }
+
+  friend bool operator==(const Iterator& lhs, const Iterator& rhs) { return lhs.ptr_ == rhs.ptr_; }
+};
+
+template <bool IsNoexcept = false>
+struct View : std::ranges::view_base {
+  static constexpr int N = 3;
+  int a[N] = {0, 1, 2};
+  int* iter_moves = nullptr;
+
+  constexpr View() = default;
+  constexpr View(int& iter_move_invocations) : iter_moves(&iter_move_invocations) {
+  }
+
+  constexpr adl::Iterator<IsNoexcept> begin() { return adl::Iterator<IsNoexcept>(a, *iter_moves); }
+  constexpr adl::Iterator<IsNoexcept> end() { return adl::Iterator<IsNoexcept>(a + N, *iter_moves); }
+};
+
+} // namespace adl
+
+constexpr bool test() {
+  // Can use `iter_move` with `inner-iterator`; `View` is a forward range.
+  {
+    SplitViewForward v("abc def", " ");
+    auto val = *v.begin();
+
+    // Non-const iterator.
+    {
+      auto i = val.begin();
+      static_assert(std::same_as<decltype(iter_move(i)), const char &&>);
+      assert(iter_move(i) == 'a');
+    }
+
+    // Const iterator.
+    {
+      const auto i = val.begin();
+      static_assert(std::same_as<decltype(iter_move(i)), const char &&>);
+      assert(iter_move(i) == 'a');
+    }
+  }
+
+  // Can use `iter_move` with `inner-iterator`, `View` is an input range.
+  {
+    SplitViewInput v("abc def", ' ');
+    auto val = *v.begin();
+
+    // Non-const iterator.
+    {
+      auto i = val.begin();
+      static_assert(std::same_as<decltype(iter_move(i)), char &&>);
+      assert(iter_move(i) == 'a');
+    }
+
+    // Const iterator.
+    {
+      const auto i = val.begin();
+      static_assert(std::same_as<decltype(iter_move(i)), char &&>);
+      assert(iter_move(i) == 'a');
+    }
+  }
+
+  // Ensure the `iter_move` customization point is being used.
+  {
+    int iter_move_invocations = 0;
+    adl::View<> input(iter_move_invocations);
+    std::ranges::lazy_split_view<adl::View<>, adl::View<>> v(input, adl::View<>());
+
+    auto val = *v.begin();
+    auto i = val.begin();
+    int x = iter_move(i);
+    assert(x == 0);
+    assert(iter_move_invocations == 1);
+  }
+
+  // Check the `noexcept` specification.
+  {
+    {
+      using ThrowingSplitView = std::ranges::lazy_split_view<adl::View<false>, adl::View<false>>;
+      using ThrowingValueType = decltype(*std::declval<ThrowingSplitView>().begin());
+      using ThrowingIter = std::ranges::iterator_t<ThrowingValueType>;
+      ASSERT_NOT_NOEXCEPT(std::ranges::iter_move(std::declval<adl::Iterator<false>>()));
+      ASSERT_NOT_NOEXCEPT(iter_move(std::declval<ThrowingIter>()));
+    }
+
+    {
+      using NoexceptSplitView = std::ranges::lazy_split_view<adl::View<true>, adl::View<true>>;
+      using NoexceptValueType = decltype(*std::declval<NoexceptSplitView>().begin());
+      using NoexceptIter = std::ranges::iterator_t<NoexceptValueType>;
+      ASSERT_NOEXCEPT(std::ranges::iter_move(std::declval<adl::Iterator<true>>()));
+      ASSERT_NOEXCEPT(iter_move(std::declval<NoexceptIter>()));
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/iter_swap.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/iter_swap.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/iter_swap.pass.cpp
@@ -0,0 +1,193 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// friend constexpr void iter_swap(const inner-iterator& x, const inner-iterator& y)
+//   noexcept(noexcept(ranges::iter_swap(x.i_.<current>, y.i_.<current>)))
+//   requires indirectly_swappable<iterator_t<Base>>;
+
+#include <ranges>
+
+#include <cassert>
+#include <type_traits>
+#include <utility>
+#include "../types.h"
+
+#include <iostream>
+
+namespace adl {
+
+template <bool IsNoexcept = false>
+struct Iterator {
+  using value_type = int;
+  using difference_type = ptrdiff_t;
+
+  value_type* ptr_ = nullptr;
+  int* iter_swap_invocations_ = nullptr;
+
+  constexpr Iterator() = default;
+  constexpr explicit Iterator(int& iter_swaps) : iter_swap_invocations_(&iter_swaps) {}
+
+  value_type& operator*() const { return *ptr_; }
+
+  Iterator& operator++() { ++ptr_; return *this; }
+  Iterator operator++(int) {
+    Iterator prev = *this;
+    ++ptr_;
+    return prev;
+  }
+
+  Iterator& operator--() { --ptr_; return *this; }
+  Iterator operator--(int) {
+    Iterator prev = *this;
+    --ptr_;
+    return prev;
+  }
+
+  constexpr friend void iter_swap(Iterator a, Iterator) noexcept(IsNoexcept) {
+    if (a.iter_swap_invocations_) {
+      ++(*a.iter_swap_invocations_);
+    }
+  }
+
+  friend bool operator==(const Iterator& lhs, const Iterator& rhs) { return lhs.ptr_ == rhs.ptr_; }
+};
+
+template <bool IsNoexcept = false>
+struct View : std::ranges::view_base {
+  int* iter_swaps = nullptr;
+
+  constexpr View() = default;
+  constexpr View(int& iter_swap_invocations) : iter_swaps(&iter_swap_invocations) {
+  }
+
+  constexpr adl::Iterator<IsNoexcept> begin() { return adl::Iterator<IsNoexcept>(*iter_swaps); }
+  constexpr adl::Iterator<IsNoexcept> end() { return adl::Iterator<IsNoexcept>(*iter_swaps); }
+};
+
+} // namespace adl
+
+constexpr bool test() {
+  // Can use `iter_swap` with `inner-iterator`; `View` is a forward range.
+  {
+    // Non-const iterator.
+    {
+      SplitViewDiff v("abc def", " ");
+      auto val = *v.begin();
+
+      auto i1 = val.begin();
+      auto i2 = i1++;
+      static_assert(std::is_void_v<decltype(iter_swap(i1, i2))>);
+      assert(*i1 == 'b');
+      assert(*i2 == 'a');
+
+      iter_swap(i1, i2);
+      assert(*i1 == 'a');
+      assert(*i2 == 'b');
+    }
+
+    // Const iterator.
+    {
+      SplitViewDiff v("abc def", " ");
+      auto val = *v.begin();
+
+      auto i1 = val.begin();
+      const auto i2 = i1++;
+      static_assert(std::is_void_v<decltype(iter_swap(i1, i2))>);
+      static_assert(std::is_void_v<decltype(iter_swap(i2, i2))>);
+      assert(*i1 == 'b');
+      assert(*i2 == 'a');
+
+      iter_swap(i1, i2);
+      assert(*i1 == 'a');
+      assert(*i2 == 'b');
+    }
+  }
+
+  // Can use `iter_swap` with `inner-iterator`; `View` is an input range.
+  {
+
+    // Non-const iterator.
+    {
+      SplitViewInput v("abc def", ' ');
+      auto val = *v.begin();
+
+      auto i1 = val.begin();
+      auto i2 = i1;
+      ++i1;
+      static_assert(std::is_void_v<decltype(iter_swap(i1, i2))>);
+      assert(*i1 == 'b');
+      // For an input view, all inner iterators are essentially thin proxies to the same underlying iterator.
+      assert(*i2 == 'b');
+
+      iter_swap(i1, i2);
+      assert(*i1 == 'b');
+      assert(*i2 == 'b');
+    }
+
+    // Const iterator.
+    {
+      SplitViewInput v("abc def", ' ');
+      auto val = *v.begin();
+
+      const auto i1 = val.begin();
+      const auto i2 = i1;
+      static_assert(std::is_void_v<decltype(iter_swap(i1, i2))>);
+      assert(*i1 == 'a');
+      assert(*i2 == 'a');
+
+      iter_swap(i1, i2);
+      assert(*i1 == 'a');
+      assert(*i2 == 'a');
+    }
+  }
+
+  // Ensure the `iter_swap` customization point is being used.
+  {
+    int iter_swap_invocations = 0;
+    adl::View<> input(iter_swap_invocations);
+    std::ranges::lazy_split_view<adl::View<>, adl::View<>> v(input, adl::View<>());
+
+    auto val = *v.begin();
+    auto i = val.begin();
+    iter_swap(i, i);
+    assert(iter_swap_invocations == 1);
+  }
+
+  // Check the `noexcept` specification.
+  {
+    {
+      using ThrowingSplitView = std::ranges::lazy_split_view<adl::View<false>, adl::View<false>>;
+      using ThrowingValueType = decltype(*std::declval<ThrowingSplitView>().begin());
+      using ThrowingIter = std::ranges::iterator_t<ThrowingValueType>;
+      ASSERT_NOT_NOEXCEPT(
+          std::ranges::iter_swap(std::declval<adl::Iterator<false>>(), std::declval<adl::Iterator<false>>()));
+      ASSERT_NOT_NOEXCEPT(iter_swap(std::declval<ThrowingIter>(), std::declval<ThrowingIter>()));
+    }
+
+    {
+      using NoexceptSplitView = std::ranges::lazy_split_view<adl::View<true>, adl::View<true>>;
+      using NoexceptValueType = decltype(*std::declval<NoexceptSplitView>().begin());
+      using NoexceptIter = std::ranges::iterator_t<NoexceptValueType>;
+      ASSERT_NOEXCEPT(
+          std::ranges::iter_swap(std::declval<adl::Iterator<true>>(), std::declval<adl::Iterator<true>>()));
+      ASSERT_NOEXCEPT(iter_swap(std::declval<NoexceptIter>(), std::declval<NoexceptIter>()));
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/types.compile.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/types.compile.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.inner/types.compile.pass.cpp
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+//  using iterator_category = If<
+//    derived_from<typename iterator_traits<iterator_t<Base>>::iterator_category, forward_iterator_tag>,
+//    forward_iterator_tag,
+//    typename iterator_traits<iterator_t<Base>>::iterator_category
+//  >;
+//  using iterator_concept = typename outer-iterator<Const>::iterator_concept;
+//  using value_type = range_value_t<Base>;
+//  using difference_type = range_difference_t<Base>;
+
+#include <ranges>
+
+#include <concepts>
+#include <iterator>
+#include "../types.h"
+
+template <class Range, class Pattern>
+using OuterIter = std::ranges::iterator_t<std::ranges::lazy_split_view<Range, Pattern>>;
+template <class Range, class Pattern>
+using InnerIter = std::ranges::iterator_t<decltype(*OuterIter<Range, Pattern>())>;
+
+// iterator_concept
+
+static_assert(std::same_as<typename InnerIter<ForwardView, ForwardView>::iterator_concept,
+    typename OuterIter<ForwardView, ForwardView>::iterator_concept>);
+static_assert(std::same_as<typename InnerIter<InputView, ForwardTinyView>::iterator_concept,
+    typename OuterIter<InputView, ForwardTinyView>::iterator_concept>);
+
+// iterator_category
+
+static_assert(std::same_as<typename InnerIter<ForwardView, ForwardView>::iterator_category, std::forward_iterator_tag>);
+
+template <class Range, class Pattern>
+concept NoIteratorCategory = !requires { typename InnerIter<Range, Pattern>::iterator_category; };
+static_assert(NoIteratorCategory<InputView, ForwardTinyView>);
+
+// value_type
+
+static_assert(std::same_as<typename InnerIter<ForwardView, ForwardView>::value_type,
+    std::ranges::range_value_t<ForwardView>>);
+
+// difference_type
+
+static_assert(std::same_as<typename InnerIter<ForwardView, ForwardView>::difference_type,
+    std::ranges::range_difference_t<ForwardView>>);
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/begin.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/begin.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/begin.pass.cpp
@@ -0,0 +1,72 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// std::ranges::lazy_split_view::outer-iterator::value_type::begin()
+
+#include <ranges>
+
+#include <cassert>
+#include "../types.h"
+
+constexpr bool operator==(const cpp20_input_iterator<char*>& lhs, const cpp20_input_iterator<char*>& rhs) {
+  return base(lhs) == base(rhs);
+}
+
+constexpr bool test() {
+  // `View` is a forward range.
+  {
+    CopyableView input("a");
+
+    // Non-const.
+    {
+      SplitViewCopyable v(input, "b");
+      auto val = *v.begin();
+      assert(val.begin().base() == input.begin());
+    }
+
+    // Const.
+    {
+      SplitViewCopyable v(input, "b");
+      const auto val = *v.begin();
+      assert(val.begin().base() == input.begin());
+    }
+  }
+
+  // `View` is an input range.
+  {
+    InputView input("a");
+
+    // Non-const.
+    {
+      SplitViewInput v(input, 'b');
+      auto val = *v.begin();
+      // Copies of `InputView` are independent and the iterators won't compare the same.
+      assert(*val.begin().base() == *input.begin());
+    }
+
+    // Const.
+    {
+      SplitViewInput v(input, 'b');
+      const auto val = *v.begin();
+      // Copies of `InputView` are independent and the iterators won't compare the same.
+      assert(*val.begin().base() == *input.begin());
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  assert(test());
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/ctor.default.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/ctor.default.pass.cpp
@@ -0,0 +1,48 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// std::ranges::lazy_split_view::outer-iterator::value_type::value_type()
+
+#include <ranges>
+
+#include "../types.h"
+
+static_assert(std::is_default_constructible_v<ValueTypeForward>);
+static_assert(std::is_default_constructible_v<ValueTypeInput>);
+
+constexpr bool test() {
+  {
+    [[maybe_unused]] ValueTypeForward val;
+  }
+
+  {
+    [[maybe_unused]] ValueTypeForward val = {};
+  }
+
+  {
+    [[maybe_unused]] ValueTypeInput val;
+  }
+
+  {
+    [[maybe_unused]] ValueTypeInput val = {};
+  }
+
+  // Note: attempting a check like `val.begin() == val.end()` leads to null pointer dereference.
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/ctor.iter.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/ctor.iter.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/ctor.iter.pass.cpp
@@ -0,0 +1,59 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// explicit outer-iterator::value_type::value_type(outer-iterator i)
+
+#include <ranges>
+
+#include <cassert>
+#include "../types.h"
+
+static_assert(std::is_constructible_v<ValueTypeForward, OuterIterForward>);
+static_assert(std::is_constructible_v<ValueTypeInput, OuterIterInput>);
+
+template <class Inner>
+void CheckCopyInitialization(Inner);
+
+template <class Value, class ConvertibleToValue>
+concept CannotCopyInitialize =
+  requires(Value v) { CheckCopyInitialization(v); } &&
+  !requires(ConvertibleToValue c) { CheckCopyInitialization<Value>(c); };
+
+// Verify that the constructor is `explicit`.
+static_assert(CannotCopyInitialize<ValueTypeForward, OuterIterForward>);
+static_assert(CannotCopyInitialize<ValueTypeInput, OuterIterInput>);
+
+constexpr bool test() {
+  // `View` is a forward range.
+  {
+    CopyableView input = "a";
+    SplitViewCopyable v(input, "b");
+    ValueTypeCopyable val(v.begin());
+    assert(val.begin().base() == input.begin());
+  }
+
+  // `View` is an input range.
+  {
+    InputView input = "a";
+    SplitViewInput v(input, 'b');
+    ValueTypeInput val(v.begin());
+    assert(*val.begin().base() == *input.begin());
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/end.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/end.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/end.pass.cpp
@@ -0,0 +1,78 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// std::ranges::lazy_split_view::outer-iterator::value_type::end()
+
+#include <ranges>
+
+#include <cassert>
+#include "../types.h"
+
+constexpr bool test() {
+  // `View` is a forward range.
+  {
+    CopyableView input("a");
+
+    // Non-const.
+    {
+      SplitViewCopyable v(input, "b");
+      auto val = *v.begin();
+
+      static_assert(std::same_as<decltype(val.end()), std::default_sentinel_t>);
+      static_assert(noexcept(val.end()));
+      [[maybe_unused]] auto e = val.end();
+    }
+
+    // Const.
+    {
+      SplitViewCopyable v(input, "b");
+      const auto val = *v.begin();
+
+      static_assert(std::same_as<decltype(val.end()), std::default_sentinel_t>);
+      static_assert(noexcept(val.end()));
+      [[maybe_unused]] auto e = val.end();
+    }
+  }
+
+  // `View` is an input range.
+  {
+    InputView input("a");
+
+    // Non-const.
+    {
+      SplitViewInput v(input, 'b');
+      auto val = *v.begin();
+
+      static_assert(std::same_as<decltype(val.end()), std::default_sentinel_t>);
+      static_assert(noexcept(val.end()));
+      [[maybe_unused]] auto e = val.end();
+    }
+
+    // Const.
+    {
+      SplitViewInput v(input, 'b');
+      const auto val = *v.begin();
+
+      static_assert(std::same_as<decltype(val.end()), std::default_sentinel_t>);
+      static_assert(noexcept(val.end()));
+      [[maybe_unused]] auto e = val.end();
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  assert(test());
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/view_interface.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/view_interface.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer.value/view_interface.pass.cpp
@@ -0,0 +1,109 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// class std::ranges::lazy_split_view::outer-iterator::value_type;
+
+#include <ranges>
+
+#include <cassert>
+#include <concepts>
+#include "../types.h"
+
+using V = ValueTypeForward;
+static_assert(std::ranges::forward_range<V>);
+static_assert(std::ranges::view<V>);
+
+// empty()
+template <class View>
+concept CanCallEmpty = requires (View& v) { v.empty(); };
+static_assert(CanCallEmpty<V>);
+
+// operator bool()
+template <class View>
+concept CanCallOperatorBool = requires (View& v) { static_cast<bool>(v); };
+static_assert(CanCallOperatorBool<V>);
+
+// front()
+template <class View>
+concept CanCallFront = requires (View& v) { v.front(); };
+static_assert(CanCallFront<V>);
+
+// size() is unavailable.
+static_assert(!std::sized_sentinel_for<std::ranges::sentinel_t<V>, std::ranges::iterator_t<V>>);
+template <class View>
+concept CanCallSize = requires (View& v) { v.size(); };
+static_assert(!CanCallSize<V>);
+
+// back() is unavailable.
+static_assert(!std::ranges::bidirectional_range<V>);
+template <class View>
+concept CanCallBack = requires (View& v) { v.back(); };
+static_assert(!CanCallBack<V>);
+
+// data() is unavailable.
+static_assert(!std::contiguous_iterator<std::ranges::iterator_t<V>>);
+template <class View>
+concept CanCallData = requires (View& v) { v.data(); };
+static_assert(!CanCallData<V>);
+
+// operator[] is unavailable.
+static_assert(!std::ranges::random_access_range<V>);
+template <class View>
+concept CanCallOperatorSubscript = requires (View& v) { v[0]; };
+static_assert(!CanCallOperatorSubscript<V>);
+
+constexpr bool test() {
+  // empty()
+  {
+    {
+      SplitViewForward v("abc def", " ");
+      auto val = *v.begin();
+      assert(!val.empty());
+    }
+
+    {
+      SplitViewForward v;
+      auto val = *v.begin();
+      assert(val.empty());
+    }
+  }
+
+  // operator bool()
+  {
+    {
+      SplitViewForward v("abc def", " ");
+      auto val = *v.begin();
+      assert(val);
+    }
+
+    {
+      SplitViewForward v;
+      auto val = *v.begin();
+      assert(!val);
+    }
+  }
+
+  // front()
+  {
+    SplitViewForward v("abc def", " ");
+    auto val = *v.begin();
+    assert(val.front() == 'a');
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.copy.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.copy.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.copy.pass.cpp
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr outer-iterator(outer-iterator<!Const> i)
+//   requires Const && convertible_to<iterator_t<View>, iterator_t<Base>>
+
+#include <ranges>
+
+#include <string_view>
+#include <type_traits>
+#include <utility>
+#include "../types.h"
+
+// outer-iterator<Const = true>
+
+template <class Iter>
+concept IsConstOuterIter = requires (Iter i) {
+  { *(*i).begin() } -> std::same_as<const char&>;
+};
+static_assert( IsConstOuterIter<OuterIterConst>);
+
+static_assert( std::convertible_to<
+    std::ranges::iterator_t<SplitViewDiff>, std::ranges::iterator_t<const SplitViewDiff>>);
+
+// outer-iterator<Const = false>
+
+template <class Iter>
+concept IsNonConstOuterIter = requires (Iter i) {
+  { *(*i).begin() } -> std::same_as<char&>;
+};
+static_assert( IsNonConstOuterIter<OuterIterNonConst>);
+
+static_assert( std::is_constructible_v<OuterIterConst, OuterIterNonConst>);
+static_assert(!std::is_constructible_v<OuterIterNonConst, OuterIterConst>);
+
+constexpr bool test() {
+  [[maybe_unused]] OuterIterConst i(OuterIterNonConst{});
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.default.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.default.pass.cpp
@@ -0,0 +1,48 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// std::ranges::lazy_split_view::outer-iterator::outer-iterator()
+
+#include <ranges>
+
+#include "../types.h"
+
+static_assert(std::is_default_constructible_v<OuterIterForward>);
+static_assert(std::is_default_constructible_v<OuterIterInput>);
+
+constexpr bool test() {
+  // `View` is a forward range.
+  {
+    [[maybe_unused]] OuterIterForward i;
+  }
+
+  {
+    [[maybe_unused]] OuterIterForward i = {};
+  }
+
+  // `View` is an input range.
+  {
+    [[maybe_unused]] OuterIterInput i;
+  }
+
+  {
+    [[maybe_unused]] OuterIterInput i = {};
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.parent.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.parent.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.parent.pass.cpp
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// std::ranges::lazy_split_view::outer-iterator::outer-iterator(Parent& parent)
+//   requires (!forward_range<Base>)
+
+#include <ranges>
+
+#include <type_traits>
+#include <utility>
+#include "../types.h"
+
+static_assert( std::ranges::forward_range<SplitViewForward>);
+static_assert(!std::is_constructible_v<OuterIterForward, SplitViewForward&>);
+
+static_assert( std::ranges::input_range<SplitViewInput>);
+static_assert( std::is_constructible_v<OuterIterInput, SplitViewInput&>);
+
+constexpr bool test() {
+  InputView input;
+  SplitViewInput v(input, ForwardTinyView());
+  [[maybe_unused]] OuterIterInput i(v);
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.parent_base.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.parent_base.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/ctor.parent_base.pass.cpp
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr outer-iterator(Parent& parent, iterator_t<Base> current);
+//   requires forward_range<Base>
+
+#include <ranges>
+
+#include <type_traits>
+#include <utility>
+#include "../types.h"
+
+static_assert( std::ranges::forward_range<SplitViewForward>);
+static_assert( std::is_constructible_v<OuterIterForward, SplitViewForward&, std::ranges::iterator_t<ForwardView>>);
+
+static_assert(!std::ranges::forward_range<SplitViewInput>);
+static_assert(!std::is_constructible_v<OuterIterInput, SplitViewInput&, std::ranges::iterator_t<InputView>>);
+
+constexpr bool test() {
+  ForwardView input("abc");
+  SplitViewForward v(std::move(input), " ");
+  [[maybe_unused]] OuterIterForward i(v, input.begin());
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/deref.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/deref.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/deref.pass.cpp
@@ -0,0 +1,72 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr outer-iterator::value-type outer-iterator::operator*() const;
+
+#include <ranges>
+
+#include <cassert>
+#include <type_traits>
+#include "../small_string.h"
+#include "../types.h"
+
+constexpr bool test() {
+  // `View` is a forward range.
+  {
+    SplitViewDiff v("abc def ghi", " ");
+
+    // Non-const iterator.
+    {
+      auto i = v.begin();
+      static_assert(!std::is_reference_v<decltype(*i)>);
+      assert(SmallString(*i) == "abc"_str);
+      assert(SmallString(*(++i)) == "def"_str);
+      assert(SmallString(*(++i)) == "ghi"_str);
+    }
+
+    // Const iterator.
+    {
+      const auto ci = v.begin();
+      static_assert(!std::is_reference_v<decltype(*ci)>);
+      assert(SmallString(*ci) == "abc"_str);
+    }
+  }
+
+  // `View` is an input range.
+  {
+    SplitViewInput v("abc def ghi", ' ');
+
+    // Non-const iterator.
+    {
+      auto i = v.begin();
+      static_assert(!std::is_reference_v<decltype(*i)>);
+      assert(SmallString(*i) == "abc"_str);
+      assert(SmallString(*(++i)) == "def"_str);
+      assert(SmallString(*(++i)) == "ghi"_str);
+    }
+
+    // Const iterator.
+    {
+      const auto ci = v.begin();
+      static_assert(!std::is_reference_v<decltype(*ci)>);
+      assert(SmallString(*ci) == "abc"_str);
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/equal.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/equal.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/equal.pass.cpp
@@ -0,0 +1,82 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// friend constexpr bool operator==(const outer-iterator& x, const outer-iterator& y)
+//   requires forward_range<Base>;
+//
+// friend constexpr bool operator==(const outer-iterator& x, default_sentinel_t);
+
+#include <ranges>
+
+#include <concepts>
+#include <string_view>
+#include "../types.h"
+
+template <class Iter>
+concept CanCallEquals = requires(const Iter& i) {
+  i == i;
+  i != i;
+};
+
+constexpr bool test() {
+  // Forward range supports both overloads of `operator==`.
+  {
+    // outer-iterator == outer-iterator
+    {
+      SplitViewForward v("abc def", " ");
+      auto b = v.begin(), e = v.end();
+
+      assert(b == b);
+      assert(!(b != b));
+
+      assert(e == e);
+      assert(!(e != e));
+
+      assert(!(b == e));
+      assert(b != e);
+    }
+
+    // outer-iterator == default_sentinel
+    {
+      SplitViewForward v("abc def", " ");
+      auto b = v.begin(), e = v.end();
+
+      assert(!(b == std::default_sentinel));
+      assert(b != std::default_sentinel);
+      assert(e == std::default_sentinel);
+      assert(!(e != std::default_sentinel));
+    }
+  }
+
+  // Input range only supports comparing an `outer-iterator` to the default sentinel.
+  {
+    using namespace std::string_view_literals;
+    SplitViewInput v("abc def"sv, ' ');
+    auto b = v.begin();
+    std::same_as<std::default_sentinel_t> auto e = v.end();
+
+    static_assert(!CanCallEquals<decltype(b)>);
+
+    assert(!(b == std::default_sentinel));
+    assert(b != std::default_sentinel);
+    assert(!(b == e));
+    assert(b != e);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/increment.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/increment.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/increment.pass.cpp
@@ -0,0 +1,84 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// constexpr outer-iterator& outer-iterator::operator++();
+// constexpr decltype(auto) outer-iterator::operator++(int);
+
+#include <ranges>
+
+#include <algorithm>
+#include <string_view>
+#include "../small_string.h"
+#include "../types.h"
+
+constexpr bool test() {
+  // Can call `outer-iterator::operator++`; `View` is a forward range.
+  {
+    SplitViewForward v("abc def ghi", " ");
+
+    // ++i
+    {
+      auto i = v.begin();
+      assert(*i == "abc"_str);
+
+      decltype(auto) i2 = ++i;
+      static_assert(std::is_lvalue_reference_v<decltype(i2)>);
+      assert(*i2 == "def"_str);
+    }
+
+    // i++
+    {
+      auto i = v.begin();
+      assert(*i == "abc"_str);
+
+      decltype(auto) i2 = i++;
+      static_assert(!std::is_reference_v<decltype(i2)>);
+      assert(*i2 == "abc"_str);
+      assert(*i == "def"_str);
+    }
+  }
+
+  // Can call `outer-iterator::operator++`; `View` is an input range.
+  {
+    // ++i
+    {
+      SplitViewInput v("abc def ghi", ' ');
+
+      auto i = v.begin();
+      assert(*i == "abc"_str);
+
+      decltype(auto) i2 = ++i;
+      static_assert(std::is_lvalue_reference_v<decltype(i2)>);
+      assert(*i2 == "def"_str);
+    }
+
+    // i++
+    {
+      SplitViewInput v("abc def ghi", ' ');
+
+      auto i = v.begin();
+      assert(*i == "abc"_str);
+
+      static_assert(std::is_void_v<decltype(i++)>);
+      i++;
+      assert(*i == "def"_str);
+    }
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/types.compile.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/types.compile.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/range.lazy.split.outer/types.compile.pass.cpp
@@ -0,0 +1,41 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// using iterator_category = input_iterator_tag; // Only defined if `View` is a forward range.
+// using iterator_concept = conditional_t<forward_range<Base>, forward_iterator_tag, input_iterator_tag>;
+// using difference_type = range_difference_t<Base>;
+
+#include <ranges>
+
+#include <concepts>
+#include <iterator>
+#include "../types.h"
+
+template <class Range, class Pattern>
+using OuterIter = decltype(std::declval<std::ranges::lazy_split_view<Range, Pattern>>().begin());
+
+// iterator_category
+
+static_assert(std::same_as<typename OuterIter<ForwardView, ForwardView>::iterator_category, std::input_iterator_tag>);
+
+template <class Range, class Pattern>
+concept NoIteratorCategory = !requires { typename OuterIter<Range, Pattern>::iterator_category; };
+static_assert(NoIteratorCategory<InputView, ForwardTinyView>);
+
+// iterator_concept
+
+static_assert(std::same_as<typename OuterIter<ForwardView, ForwardView>::iterator_concept, std::forward_iterator_tag>);
+static_assert(std::same_as<typename OuterIter<InputView, ForwardTinyView>::iterator_concept, std::input_iterator_tag>);
+
+// difference_type
+
+static_assert(std::same_as<typename OuterIter<ForwardView, ForwardView>::difference_type,
+    std::ranges::range_difference_t<ForwardView>>);
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/small_string.h b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/small_string.h
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/small_string.h
@@ -0,0 +1,82 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_LAZY_SPLIT_SMALL_STRING_H
+#define TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_LAZY_SPLIT_SMALL_STRING_H
+
+#include <algorithm>
+#include <cassert>
+#include <ranges>
+#include <string_view>
+
+// A constexpr-friendly lightweight string, primarily useful for comparisons.
+// Unlike `std::string`, all functions are `constexpr`. Unlike `std::string_view`, it copies the given string into an
+// internal buffer and can work with non-contiguous inputs.
+class SmallString {
+  constexpr static int N = 32;
+  char buffer_[N] = {};
+  size_t size_ = 0;
+
+  constexpr void omit_terminating_null() {
+    if (size_ > 0 && buffer_[size_ - 1] == '\0') {
+      --size_;
+    }
+  }
+
+public:
+  // Main constructors.
+
+  constexpr SmallString() = default;
+
+  constexpr SmallString(std::string_view v) : size_(v.size()) {
+    assert(size_ < N);
+    if (size_ == 0) return;
+
+    std::copy(v.begin(), v.end(), buffer_);
+    omit_terminating_null();
+  }
+
+  template <class I, class S>
+  constexpr SmallString(I b, const S& e) {
+    for (; b != e; ++b) {
+      buffer_[size_++] = *b;
+      assert(size_ < N);
+    }
+
+    omit_terminating_null();
+  }
+
+  // Delegating constructors.
+
+  constexpr SmallString(const char* ptr, size_t size) : SmallString(std::string_view(ptr, size)) {
+  }
+
+  template <std::ranges::range R>
+  constexpr SmallString(R&& from) : SmallString(from.begin(), from.end()) {
+  }
+
+  // Iterators.
+
+  constexpr char* begin() { return buffer_; }
+  constexpr char* end() { return buffer_ + size_; }
+  constexpr const char* begin() const { return buffer_; }
+  constexpr const char* end() const { return buffer_ + size_; }
+
+  friend constexpr bool operator==(const SmallString& lhs, const SmallString& rhs) {
+    return lhs.size_ == rhs.size_ && std::equal(lhs.buffer_, lhs.buffer_ + lhs.size_, rhs.buffer_);
+  }
+  friend constexpr bool operator==(const SmallString& lhs, std::string_view rhs) {
+    return lhs == SmallString(rhs);
+  }
+};
+
+constexpr SmallString operator "" _str(const char* ptr, size_t size) {
+  return SmallString(ptr, size);
+}
+
+#endif // TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_LAZY_SPLIT_SMALL_STRING_H
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/types.h b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/types.h
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/types.h
@@ -0,0 +1,202 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_LAZY_SPLIT_TYPES_H
+#define TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_LAZY_SPLIT_TYPES_H
+
+#include <concepts>
+#include <ranges>
+#include <string_view>
+#include <type_traits>
+#include "small_string.h"
+#include "test_macros.h"
+#include "test_iterators.h"
+
+// CopyableView
+
+struct CopyableView : std::ranges::view_base {
+  std::string_view view_;
+  constexpr explicit CopyableView() = default;
+  constexpr CopyableView(const char* ptr) : view_(ptr) {}
+  constexpr CopyableView(std::string_view v) : view_(v) {}
+  constexpr forward_iterator<const char*> begin() const { return forward_iterator<const char*>(view_.begin()); }
+  constexpr forward_iterator<const char*> end() const { return forward_iterator<const char*>(view_.end()); }
+  constexpr bool operator==(const CopyableView& rhs) const { return view_ == rhs.view_; }
+};
+static_assert( std::ranges::forward_range<CopyableView>);
+static_assert( std::ranges::forward_range<const CopyableView>);
+static_assert( std::ranges::view<CopyableView>);
+static_assert( std::is_copy_constructible_v<CopyableView>);
+
+// ForwardView
+
+struct ForwardView : std::ranges::view_base {
+  std::string_view view_;
+  constexpr explicit ForwardView() = default;
+  constexpr ForwardView(const char* ptr) : view_(ptr) {}
+  constexpr ForwardView(std::string_view v) : view_(v) {}
+  constexpr ForwardView(ForwardView&&) = default;
+  constexpr ForwardView& operator=(ForwardView&&) = default;
+  constexpr forward_iterator<const char*> begin() const { return forward_iterator<const char*>(view_.begin()); }
+  constexpr forward_iterator<const char*> end() const { return forward_iterator<const char*>(view_.end()); }
+};
+static_assert( std::ranges::forward_range<ForwardView>);
+static_assert( std::ranges::forward_range<const ForwardView>);
+static_assert( std::ranges::view<ForwardView>);
+static_assert(!std::is_copy_constructible_v<ForwardView>);
+static_assert( std::is_move_constructible_v<ForwardView>);
+
+// ForwardDiffView
+
+// Iterator types differ based on constness of this class.
+struct ForwardDiffView : std::ranges::view_base {
+  SmallString buffer_;
+  constexpr explicit ForwardDiffView() = default;
+  constexpr ForwardDiffView(const char* ptr) : ForwardDiffView(std::string_view(ptr)) {}
+  constexpr ForwardDiffView(std::string_view v) : buffer_(v) {}
+  constexpr ForwardDiffView(ForwardDiffView&&) = default;
+  constexpr ForwardDiffView& operator=(ForwardDiffView&&) = default;
+  constexpr ForwardDiffView(const ForwardDiffView&) = default;
+  constexpr ForwardDiffView& operator=(const ForwardDiffView&) = default;
+  constexpr forward_iterator<char*> begin() { return forward_iterator<char*>(buffer_.begin()); }
+  constexpr forward_iterator<char*> end()  { return forward_iterator<char*>(buffer_.end()); }
+  constexpr forward_iterator<const char*> begin() const { return forward_iterator<const char*>(buffer_.begin()); }
+  constexpr forward_iterator<const char*> end() const { return forward_iterator<const char*>(buffer_.end()); }
+};
+static_assert( std::ranges::forward_range<ForwardView>);
+static_assert( std::ranges::forward_range<const ForwardView>);
+static_assert( std::ranges::view<ForwardView>);
+static_assert(!std::same_as<std::ranges::iterator_t<ForwardDiffView>, std::ranges::iterator_t<const ForwardDiffView>>);
+
+// ForwardOnlyIfNonConstView
+
+template <class It>
+class almost_forward_iterator {
+    It it_;
+
+    template <class U> friend class almost_forward_iterator;
+
+public:
+    using iterator_category = std::forward_iterator_tag;
+    using value_type = typename std::iterator_traits<It>::value_type;
+    using difference_type = typename std::iterator_traits<It>::difference_type;
+    using pointer = It;
+    using reference = typename std::iterator_traits<It>::reference;
+
+    constexpr almost_forward_iterator() : it_() {}
+    constexpr explicit almost_forward_iterator(It it) : it_(it) {}
+    template <class U>
+        constexpr almost_forward_iterator(const almost_forward_iterator<U>& u) : it_(u.it_) {}
+
+    constexpr reference operator*() const { return *it_; }
+    constexpr pointer operator->() const { return it_; }
+
+    constexpr almost_forward_iterator& operator++() { ++it_; return *this; }
+    // Notice the slightly different return type.
+    constexpr const almost_forward_iterator operator++(int) { return almost_forward_iterator(it_); }
+
+    friend constexpr bool operator==(const almost_forward_iterator& x, const almost_forward_iterator& y) {
+      return x.it_ == y.it_;
+    }
+    friend constexpr bool operator!=(const almost_forward_iterator& x, const almost_forward_iterator& y) {
+      return x.it_ != y.it_;
+    }
+};
+static_assert(!std::forward_iterator<almost_forward_iterator<int*>>);
+static_assert( std::input_iterator<almost_forward_iterator<int*>>);
+
+struct ForwardOnlyIfNonConstView : std::ranges::view_base {
+  std::string_view view_;
+
+  constexpr explicit ForwardOnlyIfNonConstView() = default;
+  constexpr ForwardOnlyIfNonConstView(const char* ptr) : view_(ptr) {}
+  constexpr ForwardOnlyIfNonConstView(std::string_view v) : view_(v) {}
+  constexpr ForwardOnlyIfNonConstView(ForwardOnlyIfNonConstView&&) = default;
+  constexpr ForwardOnlyIfNonConstView& operator=(ForwardOnlyIfNonConstView&&) = default;
+
+  constexpr forward_iterator<const char*> begin() { return forward_iterator<const char*>(view_.begin()); }
+  constexpr forward_iterator<const char*> end() { return forward_iterator<const char*>(view_.end()); }
+  constexpr almost_forward_iterator<const char*> begin() const {
+    return almost_forward_iterator<const char*>(view_.begin());
+  }
+  constexpr almost_forward_iterator<const char*> end() const {
+    return almost_forward_iterator<const char*>(view_.end());
+  }
+};
+static_assert( std::ranges::forward_range<ForwardOnlyIfNonConstView>);
+static_assert(!std::ranges::forward_range<const ForwardOnlyIfNonConstView>);
+static_assert( std::ranges::view<ForwardOnlyIfNonConstView>);
+
+// InputView
+
+struct InputView : std::ranges::view_base {
+  SmallString buffer_;
+
+  constexpr InputView() = default;
+  constexpr InputView(const char* s) : InputView(std::string_view(s)) {}
+  constexpr InputView(std::string_view v) : buffer_(v) {}
+
+  constexpr cpp20_input_iterator<char*> begin() { return cpp20_input_iterator<char*>(buffer_.begin()); }
+  constexpr char* end() { return buffer_.end(); }
+  constexpr cpp20_input_iterator<const char*> begin() const {
+    return cpp20_input_iterator<const char*>(buffer_.begin());
+  }
+  constexpr const char* end() const { return buffer_.end(); }
+};
+constexpr bool operator==(const cpp20_input_iterator<char*>& lhs, char* rhs) { return base(lhs) == rhs; }
+constexpr bool operator==(char* lhs, const cpp20_input_iterator<char*>& rhs) { return base(rhs) == lhs; }
+constexpr bool operator==(const cpp20_input_iterator<const char*>& lhs, const char* rhs) { return base(lhs) == rhs; }
+constexpr bool operator==(const char* lhs, const cpp20_input_iterator<const char*>& rhs) { return base(rhs) == lhs; }
+
+static_assert(std::ranges::input_range<InputView>);
+static_assert(std::ranges::input_range<const InputView>);
+static_assert(std::ranges::view<InputView>);
+
+// ForwardTinyView
+
+struct ForwardTinyView : std::ranges::view_base {
+  char c_[1] = {};
+  constexpr ForwardTinyView() = default;
+  constexpr ForwardTinyView(char c) { *c_ = c; }
+  constexpr forward_iterator<const char*> begin() const { return forward_iterator<const char*>(c_); }
+  constexpr forward_iterator<const char*> end() const { return forward_iterator<const char*>(c_ + 1); }
+  constexpr static size_t size() { return 1; }
+};
+static_assert(std::ranges::forward_range<ForwardTinyView>);
+static_assert(std::ranges::view<ForwardTinyView>);
+LIBCPP_STATIC_ASSERT(std::ranges::__tiny_range<ForwardTinyView>);
+
+// Aliases
+
+using SplitViewCopyable = std::ranges::lazy_split_view<CopyableView, CopyableView>;
+using OuterIterCopyable = std::ranges::iterator_t<SplitViewCopyable>;
+using ValueTypeCopyable = decltype(*std::declval<SplitViewCopyable>().begin());
+using InnerIterCopyable = std::ranges::iterator_t<ValueTypeCopyable>;
+using BaseIterCopyable = std::ranges::iterator_t<CopyableView>;
+
+using SplitViewForward = std::ranges::lazy_split_view<ForwardView, ForwardView>;
+using OuterIterForward = std::ranges::iterator_t<SplitViewForward>;
+using ValueTypeForward = decltype(*std::declval<SplitViewForward>().begin());
+using InnerIterForward = std::ranges::iterator_t<ValueTypeForward>;
+using BaseIterForward = std::ranges::iterator_t<ForwardView>;
+
+using SplitViewInput = std::ranges::lazy_split_view<InputView, ForwardTinyView>;
+using OuterIterInput = std::ranges::iterator_t<SplitViewInput>;
+using ValueTypeInput = decltype(*std::declval<SplitViewInput>().begin());
+using InnerIterInput = std::ranges::iterator_t<ValueTypeInput>;
+using BaseIterInput = std::ranges::iterator_t<InputView>;
+
+using SplitViewDiff = std::ranges::lazy_split_view<ForwardDiffView, ForwardDiffView>;
+using OuterIterConst = decltype(std::declval<const SplitViewDiff>().begin());
+using OuterIterNonConst = decltype(std::declval<SplitViewDiff>().begin());
+static_assert(!std::same_as<OuterIterConst, OuterIterNonConst>);
+using InnerIterConst = decltype((*std::declval<OuterIterConst>()).begin());
+using InnerIterNonConst = decltype((*std::declval<OuterIterNonConst>()).begin());
+static_assert(!std::same_as<InnerIterConst, InnerIterNonConst>);
+
+#endif // TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_LAZY_SPLIT_TYPES_H
diff --git a/libcxx/test/std/ranges/range.adaptors/range.lazy.split/view_interface.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/view_interface.pass.cpp
new file mode 100644
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.lazy.split/view_interface.pass.cpp
@@ -0,0 +1,110 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-has-no-incomplete-ranges
+
+// class std::ranges::lazy_split_view;
+
+#include <ranges>
+
+#include <cassert>
+#include <concepts>
+#include <string_view>
+#include "types.h"
+
+using V = SplitViewForward;
+static_assert(std::ranges::forward_range<V>);
+
+// empty()
+template <class View>
+concept CanCallEmpty = requires (View& v) { v.empty(); };
+static_assert(CanCallEmpty<V>);
+
+// operator bool()
+template <class View>
+concept CanCallOperatorBool = requires (View& v) { static_cast<bool>(v); };
+static_assert(CanCallOperatorBool<V>);
+
+// front()
+template <class View>
+concept CanCallFront = requires (View& v) { v.front(); };
+static_assert(CanCallFront<V>);
+
+// size() is unavailable.
+static_assert(!std::sized_sentinel_for<std::ranges::sentinel_t<V>, std::ranges::iterator_t<V>>);
+template <class View>
+concept CanCallSize = requires (View& v) { v.size(); };
+static_assert(!CanCallSize<V>);
+
+// back() is unavailable.
+static_assert(!std::ranges::bidirectional_range<V>);
+template <class View>
+concept CanCallBack = requires (View& v) { v.back(); };
+static_assert(!CanCallBack<V>);
+
+// data() is unavailable.
+static_assert(!std::contiguous_iterator<std::ranges::iterator_t<V>>);
+template <class View>
+concept CanCallData = requires (View& v) { v.data(); };
+static_assert(!CanCallData<V>);
+
+// operator[] is unavailable.
+static_assert(!std::ranges::random_access_range<V>);
+template <class View>
+concept CanCallOperatorSubscript = requires (View& v) { v[0]; };
+static_assert(!CanCallOperatorSubscript<V>);
+
+constexpr bool test() {
+  using namespace std::string_view_literals;
+
+  // empty()
+  {
+    {
+      std::ranges::lazy_split_view v("abc def", " ");
+      assert(!v.empty());
+    }
+
+    {
+      // Note: an empty string literal would still produce a non-empty output because the terminating zero is treated as
+      // a separate character; hence the use of `string_view`.
+      std::ranges::lazy_split_view v(""sv, "");
+      assert(v.empty());
+    }
+  }
+
+  // operator bool()
+  {
+    {
+      std::ranges::lazy_split_view v("abc", "");
+      assert(v);
+    }
+
+    {
+      // Note: an empty string literal would still produce a non-empty output because the terminating zero is treated as
+      // a separate character; hence the use of `string_view`.
+      std::ranges::lazy_split_view v(""sv, "");
+      assert(!v);
+    }
+  }
+
+  // front()
+  {
+    SplitViewForward v("abc", "");
+    assert(*(v.front()).begin() == 'a');
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/support/test_iterators.h b/libcxx/test/support/test_iterators.h
--- a/libcxx/test/support/test_iterators.h
+++ b/libcxx/test/support/test_iterators.h
@@ -506,6 +506,9 @@
     template <class T>
     void operator,(T const &) = delete;
 };
+#ifndef _LIBCPP_HAS_NO_CONCEPTS
+  static_assert(std::input_iterator<cpp20_input_iterator<int*>>);
+#endif
 
 template<std::input_or_output_iterator>
 struct iter_value_or_void { using type = void; };