[llvm] [clang-tools-extra] [libcxx] [clang] [libc++] P2770R0: "Stashing stashing iterators for proper flattening" (PR #66033)

Jakub Mazurkiewicz via llvm-commits llvm-commits at lists.llvm.org
Tue Dec 5 04:25:12 PST 2023


https://github.com/JMazurkiewicz updated https://github.com/llvm/llvm-project/pull/66033

>From 600a282d49011782fde23c4388ba1346284153a1 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Fri, 8 Sep 2023 18:20:59 +0200
Subject: [PATCH 01/18] [libc++] P2770R0: "Stashing stashing iterators for
 proper flattening"

* Parially implements P2770R0: "Stashing stashing iterators for proper flattening"
* `join_with_view` hasn't been done yet since this type isn't implemented yet
* Rename `test/libcxx/ranges/range.adaptors/range.adaptor.tuple` directory to `test/libcxx/ranges/range.adaptors/range.adaptor.helpers` to match the standard: http://eel.is/c++draft/range.adaptor.helpers (this change happened in P2770R0, see point 3 of wording).
* Rename `libcxx\test\std\ranges\range.adaptors\range.join.view` to `libcxx\test\std\ranges\range.adaptors\range.join` to match the standard
---
 libcxx/docs/Status/Cxx23.rst                  |   1 +
 libcxx/docs/Status/Cxx23Papers.csv            |   2 +-
 libcxx/docs/UsingLibcxx.rst                   |   1 -
 libcxx/include/CMakeLists.txt                 |   1 +
 libcxx/include/__ranges/join_view.h           | 132 +++++++++++-------
 libcxx/include/__utility/as_lvalue.h          |  39 ++++++
 libcxx/include/module.modulemap.in            |   1 +
 libcxx/include/regex                          |   8 ++
 libcxx/include/utility                        |   1 +
 libcxx/modules/std/ranges.inc                 |   2 -
 .../as-lvalue.verify.cpp                      |  48 +++++++
 .../tuple-for-each.pass.cpp                   |   0
 .../segmented_iterator.compile.pass.cpp       |   1 -
 .../alg.copy/ranges.copy.segmented.pass.cpp   |   2 -
 .../alg.copy/ranges.copy_backward.pass.cpp    |   2 -
 .../iterator/ctor.parent.outer.pass.cpp       |  57 --------
 .../adaptor.pass.cpp                          |   1 -
 .../base.pass.cpp                             |   1 -
 .../begin.pass.cpp                            |  31 +++-
 .../ctad.compile.pass.cpp                     |   1 -
 .../ctad.verify.cpp                           |   1 -
 .../ctor.default.pass.cpp                     |   1 -
 .../ctor.view.pass.cpp                        |   1 -
 .../end.pass.cpp                              |  22 +--
 .../general.pass.cpp                          |   1 -
 .../range.join.iterator}/arrow.pass.cpp       |   1 -
 .../ctor.default.pass.cpp                     |  16 +--
 .../range.join.iterator}/ctor.other.pass.cpp  |   3 +-
 .../range.join.iterator}/decrement.pass.cpp   |  13 +-
 .../range.join.iterator}/eq.pass.cpp          |   7 +-
 .../range.join.iterator}/increment.pass.cpp   |  18 ++-
 .../range.join.iterator}/iter.move.pass.cpp   |   1 -
 .../range.join.iterator}/iter.swap.pass.cpp   |   1 -
 .../member_types.compile.pass.cpp             |   1 -
 .../range.join.iterator}/star.pass.cpp        |   1 -
 .../ctor.default.pass.cpp                     |   1 -
 .../range.join.sentinel}/ctor.other.pass.cpp  |   1 -
 .../range.join.sentinel}/ctor.parent.pass.cpp |   1 -
 .../range.join.sentinel}/eq.pass.cpp          |  19 ++-
 .../{range.join.view => range.join}/types.h   |  66 ++++++++-
 ...rator_concept_conformance.compile.pass.cpp |   4 +-
 .../std/re/re.iter/re.regiter/types.pass.cpp  |   4 +
 ...rator_concept_conformance.compile.pass.cpp |   4 +-
 .../std/re/re.iter/re.tokiter/types.pass.cpp  |   4 +
 libcxx/utils/data/ignore_format.txt           |  45 +++---
 45 files changed, 364 insertions(+), 205 deletions(-)
 create mode 100644 libcxx/include/__utility/as_lvalue.h
 create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
 rename libcxx/test/libcxx/ranges/range.adaptors/{range.adaptor.tuple => range.adaptor.helpers}/tuple-for-each.pass.cpp (100%)
 delete mode 100644 libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.parent.outer.pass.cpp
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/adaptor.pass.cpp (99%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/base.pass.cpp (98%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/begin.pass.cpp (83%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/ctad.compile.pass.cpp (98%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/ctad.verify.cpp (97%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/ctor.default.pass.cpp (97%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/ctor.view.pass.cpp (97%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/end.pass.cpp (95%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/general.pass.cpp (98%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/arrow.pass.cpp (99%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/ctor.default.pass.cpp (70%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/ctor.other.pass.cpp (97%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/decrement.pass.cpp (90%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/eq.pass.cpp (89%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/increment.pass.cpp (94%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/iter.move.pass.cpp (98%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/iter.swap.pass.cpp (98%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/member_types.compile.pass.cpp (99%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/iterator => range.join/range.join.iterator}/star.pass.cpp (97%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/sentinel => range.join/range.join.sentinel}/ctor.default.pass.cpp (95%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/sentinel => range.join/range.join.sentinel}/ctor.other.pass.cpp (99%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/sentinel => range.join/range.join.sentinel}/ctor.parent.pass.cpp (97%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view/sentinel => range.join/range.join.sentinel}/eq.pass.cpp (81%)
 rename libcxx/test/std/ranges/range.adaptors/{range.join.view => range.join}/types.h (88%)

diff --git a/libcxx/docs/Status/Cxx23.rst b/libcxx/docs/Status/Cxx23.rst
index 839640a7c7e88..3e6e33f08c7cc 100644
--- a/libcxx/docs/Status/Cxx23.rst
+++ b/libcxx/docs/Status/Cxx23.rst
@@ -45,6 +45,7 @@ Paper Status
       clang doesn't issue a diagnostic for deprecated using template declarations.
    .. [#note-P2520R0] P2520R0: Libc++ implemented this paper as a DR in C++20 as well.
    .. [#note-P2711R1] P2711R1: ``join_with_view`` hasn't been done yet since this type isn't implemented yet.
+   .. [#note-P2770R0] P2770R0: ``join_with_view`` hasn't been done yet since this type isn't implemented yet.
    .. [#note-P2693R1] P2693R1: The formatter for ``std::thread::id`` is implemented.
       The formatter for ``stacktrace`` is not implemented, since ``stacktrace`` is
       not implemented yet.
diff --git a/libcxx/docs/Status/Cxx23Papers.csv b/libcxx/docs/Status/Cxx23Papers.csv
index 19b1dd8eb5a44..38b77d42ec5b9 100644
--- a/libcxx/docs/Status/Cxx23Papers.csv
+++ b/libcxx/docs/Status/Cxx23Papers.csv
@@ -104,7 +104,7 @@
 "`P2708R1 <https://wg21.link/P2708R1>`__","LWG", "No Further Fundamentals TSes", "November 2022","|Nothing to do|","",""
 "","","","","","",""
 "`P0290R4 <https://wg21.link/P0290R4>`__","LWG", "``apply()`` for ``synchronized_value<T>``","February 2023","","","|concurrency TS|"
-"`P2770R0 <https://wg21.link/P2770R0>`__","LWG", "Stashing stashing ``iterators`` for proper flattening","February 2023","|In Progress|","","|ranges|"
+"`P2770R0 <https://wg21.link/P2770R0>`__","LWG", "Stashing stashing ``iterators`` for proper flattening","February 2023","|Partial| [#note-P2770R0]_","","|ranges|"
 "`P2164R9 <https://wg21.link/P2164R9>`__","LWG", "``views::enumerate``","February 2023","","","|ranges|"
 "`P2711R1 <https://wg21.link/P2711R1>`__","LWG", "Making multi-param constructors of ``views`` ``explicit``","February 2023","|In Progress| [#note-P2711R1]_","","|ranges|"
 "`P2609R3 <https://wg21.link/P2609R3>`__","LWG", "Relaxing Ranges Just A Smidge","February 2023","","","|ranges|"
diff --git a/libcxx/docs/UsingLibcxx.rst b/libcxx/docs/UsingLibcxx.rst
index 24d6a7b95f5b2..2d52a7c9ef2b8 100644
--- a/libcxx/docs/UsingLibcxx.rst
+++ b/libcxx/docs/UsingLibcxx.rst
@@ -50,7 +50,6 @@ when ``-fexperimental-library`` is passed:
 * ``std::stop_token``, ``std::stop_source`` and ``std::stop_callback``
 * ``std::jthread``
 * ``std::chrono::tzdb`` and related time zone functionality
-* ``std::ranges::join_view``
 
 .. warning::
   Experimental libraries are experimental.
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index 889d7fedbf296..7aad3a4c62434 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -837,6 +837,7 @@ set(files
   __type_traits/void_t.h
   __undef_macros
   __utility/as_const.h
+  __utility/as_lvalue.h
   __utility/auto_cast.h
   __utility/cmp.h
   __utility/convert_to_integral.h
diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index e6240dfd2580d..812a6f9379a85 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -32,6 +32,8 @@
 #include <__ranges/view_interface.h>
 #include <__type_traits/common_type.h>
 #include <__type_traits/maybe_const.h>
+#include <__utility/as_lvalue.h>
+#include <__utility/empty.h>
 #include <__utility/forward.h>
 #include <optional>
 
@@ -41,10 +43,7 @@
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
-// Note: `join_view` is still marked experimental because there is an ABI-breaking change that affects `join_view` in
-// the pipeline (https://isocpp.org/files/papers/D2770R0.html).
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-#if _LIBCPP_STD_VER >= 20 && defined(_LIBCPP_ENABLE_EXPERIMENTAL)
+#if _LIBCPP_STD_VER >= 20
 
 namespace ranges {
   template<class>
@@ -84,11 +83,16 @@ namespace ranges {
     template <class>
     friend struct std::__segmented_iterator_traits;
 
-    static constexpr bool _UseCache = !is_reference_v<_InnerRange>;
-    using _Cache = _If<_UseCache, __non_propagating_cache<remove_cvref_t<_InnerRange>>, __empty_cache>;
-    _LIBCPP_NO_UNIQUE_ADDRESS _Cache __cache_;
     _LIBCPP_NO_UNIQUE_ADDRESS _View __base_ = _View();
 
+    static constexpr bool _UseOuterCache = !forward_range<_View>;
+    using _OuterCache = _If<_UseOuterCache, __non_propagating_cache<iterator_t<_View>>, __empty_cache>;
+    _LIBCPP_NO_UNIQUE_ADDRESS _OuterCache __outer_;
+
+    static constexpr bool _UseInnerCache = !is_reference_v<_InnerRange>;
+    using _InnerCache = _If<_UseInnerCache, __non_propagating_cache<remove_cvref_t<_InnerRange>>, __empty_cache>;
+    _LIBCPP_NO_UNIQUE_ADDRESS _InnerCache __inner_;
+
   public:
     _LIBCPP_HIDE_FROM_ABI
     join_view() requires default_initializable<_View> = default;
@@ -105,16 +109,22 @@ namespace ranges {
 
     _LIBCPP_HIDE_FROM_ABI
     constexpr auto begin() {
-      constexpr bool __use_const = __simple_view<_View> &&
-                                   is_reference_v<range_reference_t<_View>>;
-      return __iterator<__use_const>{*this, ranges::begin(__base_)};
+      if constexpr (forward_range<_View>) {
+        constexpr bool __use_const = __simple_view<_View> &&
+                                     is_reference_v<range_reference_t<_View>>;
+        return __iterator<__use_const>{*this, ranges::begin(__base_)};
+      } else {
+        __outer_.__emplace(ranges::begin(__base_));
+        return __iterator<false>{*this};
+      }
     }
 
     template<class _V2 = _View>
     _LIBCPP_HIDE_FROM_ABI
     constexpr auto begin() const
-      requires input_range<const _V2> &&
-               is_reference_v<range_reference_t<const _V2>>
+      requires forward_range<const _V2> &&
+               is_reference_v<range_reference_t<const _V2>> &&
+               input_range<range_reference_t<const _V2>>
     {
       return __iterator<true>{*this, ranges::begin(__base_)};
     }
@@ -134,13 +144,12 @@ namespace ranges {
     template<class _V2 = _View>
     _LIBCPP_HIDE_FROM_ABI
     constexpr auto end() const
-      requires input_range<const _V2> &&
-               is_reference_v<range_reference_t<const _V2>>
+      requires forward_range<const _V2> &&
+               is_reference_v<range_reference_t<const _V2>> &&
+               input_range<range_reference_t<const _V2>>
     {
       using _ConstInnerRange = range_reference_t<const _View>;
-      if constexpr (forward_range<const _View> &&
-                    is_reference_v<_ConstInnerRange> &&
-                    forward_range<_ConstInnerRange> &&
+      if constexpr (forward_range<_ConstInnerRange> &&
                     common_range<const _View> &&
                     common_range<_ConstInnerRange>) {
         return __iterator<true>{*this, ranges::end(__base_)};
@@ -154,11 +163,10 @@ namespace ranges {
     requires view<_View> && input_range<range_reference_t<_View>>
   template<bool _Const>
   struct join_view<_View>::__sentinel {
-  template<bool>
-    friend struct __sentinel;
-
   private:
-    using _Parent = __maybe_const<_Const, join_view<_View>>;
+    friend join_view;
+
+    using _Parent = __maybe_const<_Const, join_view>;
     using _Base = __maybe_const<_Const, _View>;
     sentinel_t<_Base> __end_ = sentinel_t<_Base>();
 
@@ -179,7 +187,7 @@ namespace ranges {
       requires sentinel_for<sentinel_t<_Base>, iterator_t<__maybe_const<_OtherConst, _View>>>
     _LIBCPP_HIDE_FROM_ABI
     friend constexpr bool operator==(const __iterator<_OtherConst>& __x, const __sentinel& __y) {
-      return __x.__outer_ == __y.__end_;
+      return __x.__get_outer() == __y.__end_;
     }
   };
 
@@ -191,9 +199,7 @@ namespace ranges {
   template<bool _Const>
   struct join_view<_View>::__iterator final
     : public __join_view_iterator_category<__maybe_const<_Const, _View>> {
-
-    template<bool>
-    friend struct __iterator;
+    friend join_view;
 
     template <class>
     friend struct std::__segmented_iterator_traits;
@@ -209,8 +215,11 @@ namespace ranges {
 
     static constexpr bool __ref_is_glvalue = is_reference_v<range_reference_t<_Base>>;
 
+    static constexpr bool _OuterPresent = forward_range<_Base>;
+    using _OuterType                    = _If<_OuterPresent, _Outer, std::__empty>;
+
   public:
-    _Outer __outer_ = _Outer();
+    _LIBCPP_NO_UNIQUE_ADDRESS _OuterType __outer_ = _OuterType();
 
   private:
     optional<_Inner> __inner_;
@@ -218,12 +227,12 @@ namespace ranges {
 
     _LIBCPP_HIDE_FROM_ABI
     constexpr void __satisfy() {
-      for (; __outer_ != ranges::end(__parent_->__base_); ++__outer_) {
-        auto&& __inner = [&]() -> auto&& {
+      for (; __get_outer() != ranges::end(__parent_->__base_); ++__get_outer()) {
+        auto&& __inner = [this]() -> auto&& {
           if constexpr (__ref_is_glvalue)
-            return *__outer_;
+            return *__get_outer();
           else
-            return __parent_->__cache_.__emplace_from([&]() -> decltype(auto) { return *__outer_; });
+            return __parent_->__inner_.__emplace_from([&]() -> decltype(auto) { return *__get_outer(); });
         }();
         __inner_ = ranges::begin(__inner);
         if (*__inner_ != ranges::end(__inner))
@@ -234,8 +243,37 @@ namespace ranges {
         __inner_.reset();
     }
 
+    _LIBCPP_HIDE_FROM_ABI constexpr _Outer& __get_outer() {
+      if constexpr (forward_range<_Base>) {
+        return __outer_;
+      } else {
+        return *__parent_->__outer_;
+      }
+    }
+
+    _LIBCPP_HIDE_FROM_ABI constexpr const _Outer& __get_outer() const {
+      if constexpr (forward_range<_Base>) {
+        return __outer_;
+      } else {
+        return *__parent_->__outer_;
+      }
+    }
+
+    _LIBCPP_HIDE_FROM_ABI constexpr __iterator(_Parent& __parent, _Outer __outer)
+      requires forward_range<_Base>
+        : __outer_(std::move(__outer)), __parent_(std::addressof(__parent)) {
+      __satisfy();
+    }
+
+    _LIBCPP_HIDE_FROM_ABI constexpr __iterator(_Parent& __parent)
+      requires(!forward_range<_Base>)
+        : __parent_(std::addressof(__parent)) {
+      __satisfy();
+    }
+
     _LIBCPP_HIDE_FROM_ABI constexpr __iterator(_Parent* __parent, _Outer __outer, _Inner __inner)
-      : __outer_(std::move(__outer)), __inner_(std::move(__inner)), __parent_(__parent) {}
+      requires forward_range<_Base>
+        : __outer_(std::move(__outer)), __inner_(std::move(__inner)), __parent_(__parent) {}
 
   public:
     using iterator_concept = _If<
@@ -254,15 +292,7 @@ namespace ranges {
     using difference_type = common_type_t<
       range_difference_t<_Base>, range_difference_t<range_reference_t<_Base>>>;
 
-    _LIBCPP_HIDE_FROM_ABI
-    __iterator() requires default_initializable<_Outer> = default;
-
-    _LIBCPP_HIDE_FROM_ABI
-    constexpr __iterator(_Parent& __parent, _Outer __outer)
-      : __outer_(std::move(__outer))
-      , __parent_(std::addressof(__parent)) {
-      __satisfy();
-    }
+    _LIBCPP_HIDE_FROM_ABI __iterator() = default;
 
     _LIBCPP_HIDE_FROM_ABI
     constexpr __iterator(__iterator<!_Const> __i)
@@ -287,14 +317,14 @@ namespace ranges {
 
     _LIBCPP_HIDE_FROM_ABI
     constexpr __iterator& operator++() {
-      auto&& __inner = [&]() -> auto&& {
+      auto __get_inner_range = [&]() -> decltype(auto) {
         if constexpr (__ref_is_glvalue)
-          return *__outer_;
+          return *__get_outer();
         else
-          return *__parent_->__cache_;
-      }();
-      if (++*__inner_ == ranges::end(__inner)) {
-        ++__outer_;
+          return *__parent_->__inner_;
+      };
+      if (++*__inner_ == ranges::end(ranges::__as_lvalue(__get_inner_range()))) {
+        ++__get_outer();
         __satisfy();
       }
       return *this;
@@ -324,11 +354,11 @@ namespace ranges {
                common_range<range_reference_t<_Base>>
     {
       if (__outer_ == ranges::end(__parent_->__base_))
-        __inner_ = ranges::end(*--__outer_);
+        __inner_ = ranges::end(ranges::__as_lvalue(*--__outer_));
 
       // Skip empty inner ranges when going backwards.
-      while (*__inner_ == ranges::begin(*__outer_)) {
-        __inner_ = ranges::end(*--__outer_);
+      while (*__inner_ == ranges::begin(ranges::__as_lvalue(*__outer_))) {
+        __inner_ = ranges::end(ranges::__as_lvalue(*--__outer_));
       }
 
       --*__inner_;
@@ -350,7 +380,7 @@ namespace ranges {
     _LIBCPP_HIDE_FROM_ABI
     friend constexpr bool operator==(const __iterator& __x, const __iterator& __y)
       requires __ref_is_glvalue &&
-               equality_comparable<iterator_t<_Base>> &&
+               forward_range<_Base> &&
                equality_comparable<iterator_t<range_reference_t<_Base>>>
     {
       return __x.__outer_ == __y.__outer_ && __x.__inner_ == __y.__inner_;
@@ -436,7 +466,7 @@ struct __segmented_iterator_traits<_JoinViewIterator> {
   }
 };
 
-#endif // #if _LIBCPP_STD_VER >= 20 && defined(_LIBCPP_ENABLE_EXPERIMENTAL)
+#endif // #if _LIBCPP_STD_VER >= 20
 
 _LIBCPP_END_NAMESPACE_STD
 
diff --git a/libcxx/include/__utility/as_lvalue.h b/libcxx/include/__utility/as_lvalue.h
new file mode 100644
index 0000000000000..b4df650edbf65
--- /dev/null
+++ b/libcxx/include/__utility/as_lvalue.h
@@ -0,0 +1,39 @@
+// -*- 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___UTILITY_AS_LVALUE_H
+#define _LIBCPP___UTILITY_AS_LVALUE_H
+
+#include <__config>
+
+#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#  pragma GCC system_header
+#endif
+
+_LIBCPP_PUSH_MACROS
+#include <__undef_macros>
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+#if _LIBCPP_STD_VER >= 20
+
+namespace ranges {
+template <class _Tp>
+_LIBCPP_HIDE_FROM_ABI constexpr _Tp& __as_lvalue(_LIBCPP_LIFETIMEBOUND _Tp&& __t) {
+  return static_cast<_Tp&>(__t);
+}
+} // namespace ranges
+
+#endif // _LIBCPP_STD_VER >= 20
+
+_LIBCPP_END_NAMESPACE_STD
+
+_LIBCPP_POP_MACROS
+
+#endif // _LIBCPP___UTILITY_AS_LVALUE_H
diff --git a/libcxx/include/module.modulemap.in b/libcxx/include/module.modulemap.in
index 17ebe48f32996..8364dde16ccf9 100644
--- a/libcxx/include/module.modulemap.in
+++ b/libcxx/include/module.modulemap.in
@@ -2041,6 +2041,7 @@ module std_private_type_traits_unwrap_ref                                [system
 module std_private_type_traits_void_t                                    [system] { header "__type_traits/void_t.h" }
 
 module std_private_utility_as_const               [system] { header "__utility/as_const.h" }
+module std_private_utility_as_lvalue              [system] { header "__utility/as_lvalue.h" }
 module std_private_utility_auto_cast              [system] {
   header "__utility/auto_cast.h"
   export std_private_type_traits_decay
diff --git a/libcxx/include/regex b/libcxx/include/regex
index e8865ac1089d6..59d3af2a4bcb3 100644
--- a/libcxx/include/regex
+++ b/libcxx/include/regex
@@ -697,6 +697,7 @@ public:
     typedef const value_type*                    pointer;
     typedef const value_type&                    reference;
     typedef forward_iterator_tag                 iterator_category;
+    typedef input_iterator_tag                   iterator_concept; // since C++20
 
     regex_iterator();
     regex_iterator(BidirectionalIterator a, BidirectionalIterator b,
@@ -737,6 +738,7 @@ public:
     typedef const value_type*                pointer;
     typedef const value_type&                reference;
     typedef forward_iterator_tag             iterator_category;
+    typedef input_iterator_tag               iterator_concept; // since C++20
 
     regex_token_iterator();
     regex_token_iterator(BidirectionalIterator a, BidirectionalIterator b,
@@ -6407,6 +6409,9 @@ public:
     typedef const value_type*                     pointer;
     typedef const value_type&                     reference;
     typedef forward_iterator_tag                  iterator_category;
+#if _LIBCPP_STD_VER >= 20
+    typedef input_iterator_tag                    iterator_concept;
+#endif
 
 private:
     _BidirectionalIterator           __begin_;
@@ -6542,6 +6547,9 @@ public:
     typedef const value_type*                 pointer;
     typedef const value_type&                 reference;
     typedef forward_iterator_tag              iterator_category;
+#if _LIBCPP_STD_VER >= 20
+    typedef input_iterator_tag                iterator_concept;
+#endif
 
 private:
     typedef regex_iterator<_BidirectionalIterator, _CharT, _Traits> _Position;
diff --git a/libcxx/include/utility b/libcxx/include/utility
index c5581d55e79bb..1deef3db20410 100644
--- a/libcxx/include/utility
+++ b/libcxx/include/utility
@@ -249,6 +249,7 @@ template <class T>
 #include <__assert> // all public C++ headers provide the assertion handler
 #include <__config>
 #include <__utility/as_const.h>
+#include <__utility/as_lvalue.h>
 #include <__utility/auto_cast.h>
 #include <__utility/cmp.h>
 #include <__utility/declval.h>
diff --git a/libcxx/modules/std/ranges.inc b/libcxx/modules/std/ranges.inc
index a883103d81258..82c7d99f8979a 100644
--- a/libcxx/modules/std/ranges.inc
+++ b/libcxx/modules/std/ranges.inc
@@ -204,13 +204,11 @@ export namespace std {
       using std::ranges::views::drop_while;
     } // namespace views
 
-#ifdef _LIBCPP_ENABLE_EXPERIMENTAL
     using std::ranges::join_view;
 
     namespace views {
       using std::ranges::views::join;
     } // namespace views
-#endif // _LIBCPP_ENABLE_EXPERIMENTAL
 #if 0
     using std::ranges::join_with_view;
 
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
new file mode 100644
index 0000000000000..878ef91b13839
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.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
+
+// <ranges>
+
+// template<class T>
+// constexpr T& as-lvalue(T&& t) { // exposition only
+
+#include <ranges>
+
+#include <concepts>
+#include <utility>
+
+constexpr bool test() {
+  // Check glvalue
+  {
+    int lvalue{};
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::ranges::__as_lvalue(lvalue);
+  }
+
+  // Check prvalue
+  {
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::ranges::__as_lvalue(
+        0); // expected-warning {{temporary bound to local reference 'check' will be destroyed at the end of the full-expression}}
+  }
+
+  // Check xvalue
+  {
+    int xvalue{};
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::ranges::__as_lvalue(std::move(xvalue));
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.tuple/tuple-for-each.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/tuple-for-each.pass.cpp
similarity index 100%
rename from libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.tuple/tuple-for-each.pass.cpp
rename to libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/tuple-for-each.pass.cpp
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/segmented_iterator.compile.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.join/segmented_iterator.compile.pass.cpp
index 82e8cab503a27..6cd17c2b3f353 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.join/segmented_iterator.compile.pass.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/segmented_iterator.compile.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 #include <ranges>
 #include <utility>
diff --git a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy.segmented.pass.cpp b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy.segmented.pass.cpp
index 50fb479afcd06..ec60ab8db1609 100644
--- a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy.segmented.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy.segmented.pass.cpp
@@ -7,8 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 
 #include <algorithm>
 #include <array>
diff --git a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.pass.cpp b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.pass.cpp
index 184008c3e2fd0..301fbcbc533c2 100644
--- a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.pass.cpp
@@ -9,8 +9,6 @@
 // <algorithm>
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 // UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
 
 // template<bidirectional_iterator I1, sentinel_for<I1> S1, bidirectional_iterator I2>
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.parent.outer.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.parent.outer.pass.cpp
deleted file mode 100644
index 215318f15cad0..0000000000000
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.parent.outer.pass.cpp
+++ /dev/null
@@ -1,57 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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: !c++experimental
-
-// constexpr iterator(Parent& parent, OuterIter outer);
-
-#include <cassert>
-#include <ranges>
-
-#include "../types.h"
-
-using NonDefaultCtrIter = cpp20_input_iterator<int*>;
-static_assert(!std::default_initializable<NonDefaultCtrIter>);
-
-using NonDefaultCtrIterView = BufferView<NonDefaultCtrIter, sentinel_wrapper<NonDefaultCtrIter>>;
-static_assert(std::ranges::input_range<NonDefaultCtrIterView>);
-
-constexpr bool test() {
-  int buffer[4][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}};
-  {
-    CopyableChild children[4] = {CopyableChild(buffer[0]), CopyableChild(buffer[1]), CopyableChild(buffer[2]),
-                                 CopyableChild(buffer[3])};
-    CopyableParent parent{children};
-    std::ranges::join_view jv(parent);
-    std::ranges::iterator_t<decltype(jv)> iter(jv, std::ranges::begin(parent));
-    assert(*iter == 1);
-  }
-
-  {
-    // LWG3569 Inner iterator not default_initializable
-    // With the current spec, the constructor under test invokes Inner iterator's default constructor
-    // even if it is not default constructible
-    // This test is checking that this constructor can be invoked with an inner range with non default
-    // constructible iterator
-    NonDefaultCtrIterView inners[] = {buffer[0], buffer[1]};
-    auto outer = std::views::all(inners);
-    std::ranges::join_view jv(outer);
-    std::ranges::iterator_t<decltype(jv)> iter(jv, std::ranges::begin(outer));
-    assert(*iter == 1);
-  }
-
-  return true;
-}
-
-int main(int, char**) {
-  test();
-  static_assert(test());
-
-  return 0;
-}
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/adaptor.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/adaptor.pass.cpp
similarity index 99%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/adaptor.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/adaptor.pass.cpp
index afaf322721099..9beb3d282a27c 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/adaptor.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/adaptor.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // std::views::join
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/base.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/base.pass.cpp
similarity index 98%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/base.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/base.pass.cpp
index 13883e894ac7b..caf018b582263 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/base.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/base.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr V base() const& requires copy_constructible<V>;
 // constexpr V base() &&;
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/begin.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/begin.pass.cpp
similarity index 83%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/begin.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/begin.pass.cpp
index 9e4fa5f8c59a4..005d0d1d2d5cb 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/begin.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/begin.pass.cpp
@@ -7,15 +7,17 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr auto begin();
 // constexpr auto begin() const
-//    requires input_range<const V> &&
-//             is_reference_v<range_reference_t<const V>>;
+//    requires forward_range<const V> &&
+//             is_reference_v<range_reference_t<const V>> &&
+//             input_range<range_reference_t<const V>>;
 
+#include <algorithm>
 #include <cassert>
 #include <ranges>
+#include <string_view>
 
 #include "types.h"
 
@@ -120,7 +122,7 @@ constexpr bool test() {
     static_assert(HasConstBegin<decltype(jv)>);
   }
 
-  // !input_range<const V>
+  // !forward_range<const V>
   {
     std::ranges::join_view jv{ConstNotRange{}};
     static_assert(!HasConstBegin<decltype(jv)>);
@@ -146,6 +148,27 @@ constexpr bool test() {
     static_assert(std::same_as<decltype(jv.begin()), decltype(std::as_const(jv).begin())>);
   }
 
+  // Check stashing iterators (LWG3698: regex_iterator and join_view don't work together very well)
+  {
+    std::ranges::join_view<StashingRange> jv;
+    assert(std::ranges::equal(std::views::counted(jv.begin(), 10), std::string_view{"aababcabcd"}));
+  }
+
+  // LWG3700: The `const begin` of the `join_view` family does not require `InnerRng` to be a range
+  {
+    std::ranges::join_view<ConstNonJoinableRange> jv;
+    static_assert(!HasConstBegin<decltype(jv)>);
+  }
+
+  // Check example from LWG3700
+  {
+    auto r = std::views::iota(0, 5) | std::views::split(1);
+    auto s = std::views::single(r);
+    auto j = s | std::views::join;
+    auto f = j.front();
+    assert(std::ranges::equal(f, std::views::single(0)));
+  }
+
   return true;
 }
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.compile.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/ctad.compile.pass.cpp
similarity index 98%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.compile.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/ctad.compile.pass.cpp
index 2c470991be0b6..a8eafc5a9c021 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.compile.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/ctad.compile.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // template<class R>
 //   explicit join_view(R&&) -> join_view<views::all_t<R>>;
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.verify.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/ctad.verify.cpp
similarity index 97%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.verify.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/ctad.verify.cpp
index eddc950747ba7..2c6eea500580d 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.verify.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/ctad.verify.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // template<class R>
 //   explicit join_view(R&&) -> join_view<views::all_t<R>>;
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/ctor.default.pass.cpp
similarity index 97%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.default.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/ctor.default.pass.cpp
index 26206e32c358c..0daff7d3b3c98 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.default.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/ctor.default.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // join_view() requires default_initializable<V> = default;
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.view.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/ctor.view.pass.cpp
similarity index 97%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.view.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/ctor.view.pass.cpp
index ce5393062d778..75d4c7e5916b0 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.view.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/ctor.view.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr explicit join_view(V base);
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/end.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/end.pass.cpp
similarity index 95%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/end.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/end.pass.cpp
index 7e225202cc231..516ba25a0e859 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/end.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/end.pass.cpp
@@ -7,10 +7,12 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr auto end();
 // constexpr auto end() const;
+//   requires forward_range<const V> &&
+//            is_reference_v<range_reference_t<const V>> &&
+//            input_range<range_reference_t<const V>>
 
 #include <cassert>
 #include <ranges>
@@ -33,13 +35,13 @@ concept HasConstEnd = requires (const T& t){
 // | 3  |   Y    |   Y     |   Y    |   Y   |    N    |   Y    |sentinel<true> |sentinel<true>|
 // | 4  |   Y    |   Y     |   Y    |   N   |    Y    |   Y    |sentinel<true> |      -       |
 // | 5  |   Y    |   Y     |   N    |   Y   |    Y    |   Y    |sentinel<true> |sentinel<true>|
-// | 6  |   Y    |   N     |   Y    |   Y   |    Y    |   Y    |sentinel<true> |sentinel<true>|
+// | 6  |   Y    |   N     |   Y    |   Y   |    Y    |   Y    |sentinel<true> |      -       |
 // | 7  |   N    |   Y     |   Y    |   Y   |    Y    |   Y    |iterator<false>|iterator<true>|
 // | 8  |   N    |   Y     |   Y    |   Y   |    Y    |   N    |sentinel<false>|sentinel<true>|
 // | 9  |   N    |   Y     |   Y    |   Y   |    N    |   Y    |sentinel<false>|sentinel<true>|
 // | 10 |   N    |   Y     |   Y    |   N   |    Y    |   Y    |sentinel<false>|      -       |
 // | 11 |   N    |   Y     |   N    |   Y   |    Y    |   Y    |sentinel<false>|sentinel<true>|
-// | 12 |   N    |   N     |   Y    |   Y   |    Y    |   Y    |sentinel<false>|sentinel<true>|
+// | 12 |   N    |   N     |   Y    |   Y   |    Y    |   Y    |sentinel<false>|      -       |
 //
 //
 
@@ -131,10 +133,8 @@ constexpr bool test() {
 
     std::ranges::join_view jv(outer);
     assert(jv.end() == std::ranges::next(jv.begin(), 16));
-    assert(std::as_const(jv).end() == std::ranges::next(std::as_const(jv).begin(), 16));
 
-    static_assert(HasConstEnd<decltype(jv)>);
-    static_assert(std::same_as<decltype(jv.end()), decltype(std::as_const(jv).end())>);
+    static_assert(!HasConstEnd<decltype(jv)>);
     static_assert(!std::ranges::common_range<decltype(jv)>);
     static_assert(!std::ranges::common_range<const decltype(jv)>);
   }
@@ -219,10 +219,8 @@ constexpr bool test() {
 
     std::ranges::join_view jv(outer);
     assert(jv.end() == std::ranges::next(jv.begin(), 16));
-    assert(std::as_const(jv).end() == std::ranges::next(std::as_const(jv).begin(), 16));
 
-    static_assert(HasConstEnd<decltype(jv)>);
-    static_assert(!std::same_as<decltype(jv.end()), decltype(std::as_const(jv).end())>);
+    static_assert(!HasConstEnd<decltype(jv)>);
     static_assert(!std::ranges::common_range<decltype(jv)>);
     static_assert(!std::ranges::common_range<const decltype(jv)>);
   }
@@ -288,6 +286,12 @@ constexpr bool test() {
     assert(jv.end() == std::ranges::next(jv.begin(), 12));
   }
 
+  // LWG3700: The `const begin` of the `join_view` family does not require `InnerRng` to be a range
+  {
+    std::ranges::join_view<ConstNonJoinableRange> jv;
+    static_assert(!HasConstEnd<decltype(jv)>);
+  }
+
   return true;
 }
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/general.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/general.pass.cpp
similarity index 98%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/general.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/general.pass.cpp
index e9eab585260cd..f92eb418fac77 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/general.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/general.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // General tests for join_view. This file does not test anything specifically.
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/arrow.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/arrow.pass.cpp
similarity index 99%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/arrow.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/arrow.pass.cpp
index e610cde2c3b5b..ddcf66bfe775e 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/arrow.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/arrow.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr InnerIter operator->() const
 //   requires has-arrow<InnerIter> && copyable<InnerIter>;
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.default.pass.cpp
similarity index 70%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.default.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.default.pass.cpp
index e4f193e4e6064..82fe824fad1b2 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.default.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.default.pass.cpp
@@ -7,9 +7,8 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
-// iterator() requires default_initializable<OuterIter> = default;
+// iterator() = default;
 
 #include <ranges>
 
@@ -29,19 +28,12 @@ constexpr void test_default_constructible() {
   using JoinView = std::ranges::join_view<view<It>>;
   using JoinIterator = std::ranges::iterator_t<JoinView>;
   static_assert(std::is_default_constructible_v<JoinIterator>);
-  JoinIterator it; (void)it;
-}
-
-template <class It>
-constexpr void test_non_default_constructible() {
-  using JoinView = std::ranges::join_view<view<It>>;
-  using JoinIterator = std::ranges::iterator_t<JoinView>;
-  static_assert(!std::is_default_constructible_v<JoinIterator>);
+  [[maybe_unused]] JoinIterator it;
 }
 
 constexpr bool test() {
-  test_non_default_constructible<cpp17_input_iterator<ChildView*>>();
-  // NOTE: cpp20_input_iterator can't be used with join_view because it is not copyable.
+  test_default_constructible<cpp17_input_iterator<ChildView*>>();
+  test_default_constructible<cpp20_input_iterator<ChildView*>>();
   test_default_constructible<forward_iterator<ChildView*>>();
   test_default_constructible<bidirectional_iterator<ChildView*>>();
   test_default_constructible<random_access_iterator<ChildView*>>();
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.other.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.other.pass.cpp
similarity index 97%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.other.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.other.pass.cpp
index a0406f90c88c6..e220b2cfeac84 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.other.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.other.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr iterator(iterator<!Const> i)
 //             requires Const &&
@@ -37,7 +36,7 @@ constexpr bool test() {
   {
     CopyableChild children[4] = {CopyableChild(buffer[0]), CopyableChild(buffer[1]), CopyableChild(buffer[2]),
                                  CopyableChild(buffer[3])};
-    std::ranges::join_view jv(CopyableParent{children});
+    std::ranges::join_view jv(ForwardCopyableParent{children});
     auto iter1 = jv.begin();
     using iterator = decltype(iter1);
     using const_iterator = decltype(std::as_const(jv).begin());
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/decrement.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/decrement.pass.cpp
similarity index 90%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/decrement.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/decrement.pass.cpp
index 4363fb0e330c3..29720d93bab66 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/decrement.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/decrement.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr iterator& operator--();
 //              requires ref-is-glvalue && bidirectional_range<Base> &&
@@ -18,9 +17,12 @@
 //                       bidirectional_range<range_reference_t<Base>> &&
 //                       common_range<range_reference_t<Base>>;
 
+#include <algorithm>
+#include <array>
 #include <cassert>
 #include <ranges>
 #include <type_traits>
+#include <vector>
 
 #include "../types.h"
 
@@ -150,6 +152,15 @@ constexpr bool test() {
     static_assert(!CanPostDecrement<decltype(iter)>);
   }
 
+  {
+    // LWG3791: `join_view::iterator::operator--` may be ill-formed
+    std::vector<std::vector<int>> vec = {{1, 2}, {3, 4}, {5, 6}};
+    auto r = vec | std::views::transform([](auto& x) -> auto&& { return std::move(x); }) | std::views::join;
+    auto e = --r.end();
+    assert(*e == 6);
+    assert(std::ranges::equal(std::views::reverse(r), std::array{6, 5, 4, 3, 2, 1}));
+  }
+
   return true;
 }
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/eq.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
similarity index 89%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/eq.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
index 327cc82b06b08..8b7e694b080f4 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/eq.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
@@ -7,10 +7,9 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // friend constexpr bool operator==(const iterator& x, const iterator& y);
-//          requires ref-is-glvalue && equality_comparable<iterator_t<Base>> &&
+//          requires ref-is-glvalue && forward_range<Base> &&
 //                   equality_comparable<iterator_t<range_reference_t<Base>>>;
 
 #include <cassert>
@@ -43,7 +42,7 @@ constexpr bool test() {
   }
 
   {
-    // !equality_comparable<iterator_t<Base>>
+    // !forward_range<iterator_t<Base>>
     using Inner = BufferView<int*>;
     using Outer = BufferView<cpp20_input_iterator<Inner*>, sentinel_wrapper<cpp20_input_iterator<Inner*>>>;
     static_assert(!std::equality_comparable<std::ranges::iterator_t<Outer>>);
@@ -51,8 +50,6 @@ constexpr bool test() {
     std::ranges::join_view jv(Outer{inners});
     auto iter = jv.begin();
     static_assert(!std::equality_comparable<decltype(iter)>);
-    auto const_iter = std::as_const(jv).begin();
-    static_assert(!std::equality_comparable<decltype(const_iter)>);
   }
 
   {
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/increment.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/increment.pass.cpp
similarity index 94%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/increment.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/increment.pass.cpp
index 4bcb4de7e9c88..dada91462a73f 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/increment.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/increment.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr iterator& operator++();
 // constexpr void operator++(int);
@@ -205,6 +204,23 @@ constexpr bool test() {
     static_assert(std::is_void_v<decltype(iter++)>);
   }
 
+  {
+    // Check stashing iterators (LWG3698: regex_iterator and join_view don't work together very well)
+    std::ranges::join_view<StashingRange> jv;
+    auto it = jv.begin();
+    assert(*it == 'a');
+    ++it;
+    assert(*it == 'a');
+    ++it;
+    assert(*it == 'b');
+    it++;
+    assert(*it == 'a');
+    it++;
+    assert(*it == 'b');
+    ++it;
+    assert(*it == 'c');
+  }
+
   return true;
 }
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/iter.move.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/iter.move.pass.cpp
similarity index 98%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/iter.move.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/iter.move.pass.cpp
index 0bf6aa3d92614..917e72dc85854 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/iter.move.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/iter.move.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // friend constexpr decltype(auto) iter_move(const iterator& i);
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/iter.swap.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/iter.swap.pass.cpp
similarity index 98%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/iter.swap.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/iter.swap.pass.cpp
index e9b73f1a41596..28e1bf75726f6 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/iter.swap.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/iter.swap.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // friend constexpr void iter_swap(const iterator& x, const iterator& y);
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/member_types.compile.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/member_types.compile.pass.cpp
similarity index 99%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/member_types.compile.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/member_types.compile.pass.cpp
index 17b98facd6508..b9b9d73d77e26 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/member_types.compile.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/member_types.compile.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // Iterator traits and member typedefs in join_view::<iterator>.
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/star.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/star.pass.cpp
similarity index 97%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/star.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/star.pass.cpp
index fa6f7bb031207..73457b826df0b 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/star.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/star.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr decltype(auto) operator*() const;
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.default.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.default.pass.cpp
similarity index 95%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.default.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.default.pass.cpp
index 0eebe14af3fcb..42fcc733e181f 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.default.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.default.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // sentinel() = default;
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.other.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.other.pass.cpp
similarity index 99%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.other.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.other.pass.cpp
index c2d7058746d75..4bd8025efb5c1 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.other.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.other.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr sentinel(sentinel<!Const> s);
 //             requires Const && convertible_to<sentinel_t<V>, sentinel_t<Base>>;
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.parent.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.parent.pass.cpp
similarity index 97%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.parent.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.parent.pass.cpp
index a9df7c3881ba8..1ac68277338fe 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.parent.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.parent.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // constexpr explicit sentinel(Parent& parent);
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/eq.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/eq.pass.cpp
similarity index 81%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/eq.pass.cpp
rename to libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/eq.pass.cpp
index cbd03b84f208b..bc7d4bec94d3e 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/eq.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/eq.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // template<bool OtherConst>
 //   requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>>
@@ -17,6 +16,7 @@
 #include <concepts>
 #include <functional>
 #include <ranges>
+#include <type_traits>
 
 #include "../types.h"
 
@@ -61,18 +61,27 @@ static_assert(EqualityComparable<std::ranges::iterator_t<const ConstComparableVi
 constexpr bool test() {
   int buffer[4][4] = {{1111, 2222, 3333, 4444}, {555, 666, 777, 888}, {99, 1010, 1111, 1212}, {13, 14, 15, 16}};
 
+  // test iterator<false> == sentinel<false>
   {
     ChildView children[4] = {ChildView(buffer[0]), ChildView(buffer[1]), ChildView(buffer[2]), ChildView(buffer[3])};
-    auto jv = std::ranges::join_view(ParentView(children));
+    auto jv               = std::ranges::join_view(ParentView(children));
     assert(jv.end() == std::ranges::next(jv.begin(), 16));
-    static_assert(!EqualityComparable<decltype(std::as_const(jv).begin()), decltype(jv.end())>);
-    static_assert(!EqualityComparable<decltype(jv.begin()), decltype(std::as_const(jv).end())>);
   }
 
+  // test iterator<false> == sentinel<true>
+  {
+    ChildView children[4] = {ChildView(buffer[0]), ChildView(buffer[1]), ChildView(buffer[2]), ChildView(buffer[3])};
+    using ParentT         = std::remove_all_extents_t<decltype(children)>;
+    auto jv               = std::ranges::join_view(ForwardParentView<ParentT>(children));
+    assert(std::as_const(jv).end() == std::ranges::next(jv.begin(), 16));
+  }
+
+  // test iterator<true> == sentinel<true>
   {
     CopyableChild children[4] = {CopyableChild(buffer[0]), CopyableChild(buffer[1]), CopyableChild(buffer[2]),
                                  CopyableChild(buffer[3])};
-    const auto jv = std::ranges::join_view(ParentView(children));
+    using ParentT             = std::remove_all_extents_t<decltype(children)>;
+    const auto jv             = std::ranges::join_view(ForwardParentView<ParentT>(children));
     assert(jv.end() == std::ranges::next(jv.begin(), 16));
   }
 
diff --git a/libcxx/test/std/ranges/range.adaptors/range.join.view/types.h b/libcxx/test/std/ranges/range.adaptors/range.join/types.h
similarity index 88%
rename from libcxx/test/std/ranges/range.adaptors/range.join.view/types.h
rename to libcxx/test/std/ranges/range.adaptors/range.join/types.h
index b2ef5f090b573..c1378dc1144b4 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join.view/types.h
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/types.h
@@ -11,6 +11,7 @@
 
 #include <concepts>
 #include <cstdint>
+#include <string>
 #include <tuple>
 
 #include "test_macros.h"
@@ -52,13 +53,13 @@ inline ChildView globalChildren[4] = {
     ChildView(globalBuffer[3]),
 };
 
-template <class T>
+template <class T, template<class...> class Iter = cpp17_input_iterator>
 struct ParentView : std::ranges::view_base {
   T* ptr_;
   unsigned size_;
 
-  using iterator = cpp20_input_iterator<T*>;
-  using const_iterator = cpp20_input_iterator<const T*>;
+  using iterator = Iter<T*>;
+  using const_iterator = Iter<const T*>;
   using sentinel = sentinel_wrapper<iterator>;
   using const_sentinel = sentinel_wrapper<const_iterator>;
 
@@ -80,6 +81,9 @@ struct ParentView : std::ranges::view_base {
 template <class T>
 ParentView(T*) -> ParentView<T>;
 
+template<class T>
+using ForwardParentView = ParentView<T, forward_iterator>;
+
 struct CopyableChild : std::ranges::view_base {
   int* ptr_;
   unsigned size_;
@@ -97,15 +101,16 @@ struct CopyableChild : std::ranges::view_base {
   constexpr const_sentinel end() const { return const_sentinel(const_iterator(ptr_ + size_)); }
 };
 
-struct CopyableParent : std::ranges::view_base {
+template<template<class...> class Iter>
+struct CopyableParentTemplate : std::ranges::view_base {
   CopyableChild* ptr_;
 
-  using iterator = cpp17_input_iterator<CopyableChild*>;
-  using const_iterator = cpp17_input_iterator<const CopyableChild*>;
+  using iterator = Iter<CopyableChild*>;
+  using const_iterator = Iter<const CopyableChild*>;
   using sentinel = sentinel_wrapper<iterator>;
   using const_sentinel = sentinel_wrapper<const_iterator>;
 
-  constexpr CopyableParent(CopyableChild* ptr) : ptr_(ptr) {}
+  constexpr CopyableParentTemplate(CopyableChild* ptr) : ptr_(ptr) {}
 
   constexpr iterator begin() { return iterator(ptr_); }
   constexpr const_iterator begin() const { return const_iterator(ptr_); }
@@ -113,6 +118,9 @@ struct CopyableParent : std::ranges::view_base {
   constexpr const_sentinel end() const { return const_sentinel(const_iterator(ptr_ + 4)); }
 };
 
+using CopyableParent = CopyableParentTemplate<cpp17_input_iterator>;
+using ForwardCopyableParent = CopyableParentTemplate<forward_iterator>;
+
 struct Box {
   int x;
 };
@@ -392,4 +400,48 @@ struct IterMoveSwapAwareView : BufferView<int*> {
 };
 static_assert(std::ranges::input_range<IterMoveSwapAwareView>);
 
+class StashingIterator {
+public:
+  using difference_type = std::ptrdiff_t;
+  using value_type      = std::string;
+
+  constexpr StashingIterator() : letter_('a') {}
+
+  constexpr StashingIterator& operator++() {
+    str_ += letter_;
+    ++letter_;
+    return *this;
+  }
+
+  constexpr void operator++(int) { ++*this; }
+
+  constexpr value_type operator*() const { return str_; }
+
+  constexpr bool operator==(std::default_sentinel_t) const { return letter_ > 'z'; }
+
+private:
+  char letter_;
+  value_type str_;
+};
+
+using StashingRange = std::ranges::subrange<StashingIterator, std::default_sentinel_t>;
+static_assert(std::ranges::input_range<StashingRange>);
+static_assert(!std::ranges::forward_range<StashingRange>);
+
+class ConstNonJoinableRange : public std::ranges::view_base {
+public:
+  constexpr StashingIterator begin() { return {}; }
+  constexpr std::default_sentinel_t end() { return {}; }
+
+  constexpr const int* begin() const { return &val_; }
+  constexpr const int* end() const { return &val_ + 1; }
+
+private:
+  int val_ = 1;
+};
+static_assert(std::ranges::input_range<ConstNonJoinableRange>);
+static_assert(std::ranges::input_range<const ConstNonJoinableRange>);
+static_assert(std::ranges::input_range<std::ranges::range_reference_t<ConstNonJoinableRange>>);
+static_assert(!std::ranges::input_range<std::ranges::range_reference_t<const ConstNonJoinableRange>>);
+
 #endif // TEST_STD_RANGES_RANGE_ADAPTORS_RANGE_JOIN_TYPES_H
diff --git a/libcxx/test/std/re/re.iter/re.regiter/iterator_concept_conformance.compile.pass.cpp b/libcxx/test/std/re/re.iter/re.regiter/iterator_concept_conformance.compile.pass.cpp
index 6f2da091c3709..ad61baa76018d 100644
--- a/libcxx/test/std/re/re.iter/re.regiter/iterator_concept_conformance.compile.pass.cpp
+++ b/libcxx/test/std/re/re.iter/re.regiter/iterator_concept_conformance.compile.pass.cpp
@@ -14,8 +14,8 @@
 
 #include <iterator>
 
-static_assert(std::forward_iterator<std::cregex_iterator>);
-static_assert(!std::bidirectional_iterator<std::cregex_iterator>);
+static_assert(std::input_iterator<std::cregex_iterator>);
+static_assert(!std::forward_iterator<std::cregex_iterator>);
 static_assert(!std::indirectly_writable<std::cregex_iterator, char>);
 static_assert(std::sentinel_for<std::cregex_iterator, std::cregex_iterator>);
 static_assert(!std::sized_sentinel_for<std::cregex_iterator, std::cregex_iterator>);
diff --git a/libcxx/test/std/re/re.iter/re.regiter/types.pass.cpp b/libcxx/test/std/re/re.iter/re.regiter/types.pass.cpp
index 7d30b0adcc234..8ee2c5006d31c 100644
--- a/libcxx/test/std/re/re.iter/re.regiter/types.pass.cpp
+++ b/libcxx/test/std/re/re.iter/re.regiter/types.pass.cpp
@@ -20,6 +20,7 @@
 //     typedef const value_type*                    pointer;
 //     typedef const value_type&                    reference;
 //     typedef forward_iterator_tag                 iterator_category;
+//     typedef input_iterator_tag                   iterator_concept; // since C++20
 
 #include <regex>
 #include <type_traits>
@@ -36,6 +37,9 @@ test()
     static_assert((std::is_same<typename I::pointer, const std::match_results<const CharT*>*>::value), "");
     static_assert((std::is_same<typename I::reference, const std::match_results<const CharT*>&>::value), "");
     static_assert((std::is_same<typename I::iterator_category, std::forward_iterator_tag>::value), "");
+#if TEST_STD_VER >= 20
+    static_assert(std::is_same_v<typename I::iterator_concept, std::input_iterator_tag>);
+#endif
 }
 
 int main(int, char**)
diff --git a/libcxx/test/std/re/re.iter/re.tokiter/iterator_concept_conformance.compile.pass.cpp b/libcxx/test/std/re/re.iter/re.tokiter/iterator_concept_conformance.compile.pass.cpp
index 397226552edee..23eea7f369c17 100644
--- a/libcxx/test/std/re/re.iter/re.tokiter/iterator_concept_conformance.compile.pass.cpp
+++ b/libcxx/test/std/re/re.iter/re.tokiter/iterator_concept_conformance.compile.pass.cpp
@@ -14,8 +14,8 @@
 
 #include <iterator>
 
-static_assert(std::forward_iterator<std::cregex_token_iterator>);
-static_assert(!std::bidirectional_iterator<std::cregex_token_iterator>);
+static_assert(std::input_iterator<std::cregex_token_iterator>);
+static_assert(!std::forward_iterator<std::cregex_token_iterator>);
 static_assert(!std::indirectly_writable<std::cregex_token_iterator, char>);
 static_assert(std::sentinel_for<std::cregex_token_iterator, std::cregex_token_iterator>);
 static_assert(!std::sized_sentinel_for<std::cregex_token_iterator, std::cregex_token_iterator>);
diff --git a/libcxx/test/std/re/re.iter/re.tokiter/types.pass.cpp b/libcxx/test/std/re/re.iter/re.tokiter/types.pass.cpp
index 73ad58f4eecfb..a9c18e8a1b77a 100644
--- a/libcxx/test/std/re/re.iter/re.tokiter/types.pass.cpp
+++ b/libcxx/test/std/re/re.iter/re.tokiter/types.pass.cpp
@@ -20,6 +20,7 @@
 //     typedef const value_type*                pointer;
 //     typedef const value_type&                reference;
 //     typedef forward_iterator_tag             iterator_category;
+//     typedef input_iterator_tag               iterator_concept; // since C++20
 
 #include <regex>
 #include <type_traits>
@@ -36,6 +37,9 @@ test()
     static_assert((std::is_same<typename I::pointer, const std::sub_match<const CharT*>*>::value), "");
     static_assert((std::is_same<typename I::reference, const std::sub_match<const CharT*>&>::value), "");
     static_assert((std::is_same<typename I::iterator_category, std::forward_iterator_tag>::value), "");
+#if TEST_STD_VER >= 20
+    static_assert(std::is_same_v<typename I::iterator_concept, std::input_iterator_tag>);
+#endif
 }
 
 int main(int, char**)
diff --git a/libcxx/utils/data/ignore_format.txt b/libcxx/utils/data/ignore_format.txt
index 123d06d56e29a..c10745ce3edad 100644
--- a/libcxx/utils/data/ignore_format.txt
+++ b/libcxx/utils/data/ignore_format.txt
@@ -5209,29 +5209,28 @@ libcxx/test/std/ranges/range.adaptors/range.filter/sentinel/compare.pass.cpp
 libcxx/test/std/ranges/range.adaptors/range.filter/sentinel/ctor.default.pass.cpp
 libcxx/test/std/ranges/range.adaptors/range.filter/sentinel/ctor.parent.pass.cpp
 libcxx/test/std/ranges/range.adaptors/range.filter/types.h
-libcxx/test/std/ranges/range.adaptors/range.join.view/adaptor.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/base.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/begin.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.compile.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/ctad.verify.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.default.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/ctor.view.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/end.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/general.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/arrow.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.default.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.other.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/ctor.parent.outer.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/decrement.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/eq.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/increment.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/member_types.compile.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/iterator/star.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.default.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.other.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/ctor.parent.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/sentinel/eq.pass.cpp
-libcxx/test/std/ranges/range.adaptors/range.join.view/types.h
+libcxx/test/std/ranges/range.adaptors/range.join/adaptor.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/base.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/begin.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/ctad.compile.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/ctad.verify.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/ctor.default.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/ctor.view.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/end.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/general.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/arrow.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.default.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/ctor.other.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/decrement.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/increment.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/member_types.compile.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/star.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.default.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.other.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/ctor.parent.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/range.join.sentinel/eq.pass.cpp
+libcxx/test/std/ranges/range.adaptors/range.join/types.h
 libcxx/test/std/ranges/range.adaptors/range.lazy.split/adaptor.pass.cpp
 libcxx/test/std/ranges/range.adaptors/range.lazy.split/base.pass.cpp
 libcxx/test/std/ranges/range.adaptors/range.lazy.split/begin.pass.cpp

>From f601c073fdfc6fce8ec6b54bee5f93b4d87a4e6f Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 15:40:34 +0100
Subject: [PATCH 02/18] Enable `__as_lvalue` in >C++03 modes

Comments:
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396562251
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396564172
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396579715
---
 libcxx/include/__ranges/join_view.h                  |  8 ++++----
 libcxx/include/__utility/as_lvalue.h                 |  6 ++----
 .../range.adaptor.helpers/as-lvalue.verify.cpp       | 12 ++++--------
 3 files changed, 10 insertions(+), 16 deletions(-)

diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index 812a6f9379a85..fd68358837e0f 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -323,7 +323,7 @@ namespace ranges {
         else
           return *__parent_->__inner_;
       };
-      if (++*__inner_ == ranges::end(ranges::__as_lvalue(__get_inner_range()))) {
+      if (++*__inner_ == ranges::end(std::__as_lvalue(__get_inner_range()))) {
         ++__get_outer();
         __satisfy();
       }
@@ -354,11 +354,11 @@ namespace ranges {
                common_range<range_reference_t<_Base>>
     {
       if (__outer_ == ranges::end(__parent_->__base_))
-        __inner_ = ranges::end(ranges::__as_lvalue(*--__outer_));
+        __inner_ = ranges::end(std::__as_lvalue(*--__outer_));
 
       // Skip empty inner ranges when going backwards.
-      while (*__inner_ == ranges::begin(ranges::__as_lvalue(*__outer_))) {
-        __inner_ = ranges::end(ranges::__as_lvalue(*--__outer_));
+      while (*__inner_ == ranges::begin(std::__as_lvalue(*__outer_))) {
+        __inner_ = ranges::end(std::__as_lvalue(*--__outer_));
       }
 
       --*__inner_;
diff --git a/libcxx/include/__utility/as_lvalue.h b/libcxx/include/__utility/as_lvalue.h
index b4df650edbf65..159f45dad4d41 100644
--- a/libcxx/include/__utility/as_lvalue.h
+++ b/libcxx/include/__utility/as_lvalue.h
@@ -21,16 +21,14 @@ _LIBCPP_PUSH_MACROS
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
-#if _LIBCPP_STD_VER >= 20
+#ifndef _LIBCPP_CXX03_LANG
 
-namespace ranges {
 template <class _Tp>
 _LIBCPP_HIDE_FROM_ABI constexpr _Tp& __as_lvalue(_LIBCPP_LIFETIMEBOUND _Tp&& __t) {
   return static_cast<_Tp&>(__t);
 }
-} // namespace ranges
 
-#endif // _LIBCPP_STD_VER >= 20
+#endif // !_LIBCPP_CXX03_LANG
 
 _LIBCPP_END_NAMESPACE_STD
 
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
index 878ef91b13839..8efd7e5ccf983 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
@@ -6,15 +6,11 @@
 //
 //===----------------------------------------------------------------------===//
 
-// UNSUPPORTED: c++03, c++11, c++14, c++17
-
-// <ranges>
+// UNSUPPORTED: c++03
 
 // template<class T>
 // constexpr T& as-lvalue(T&& t) { // exposition only
 
-#include <ranges>
-
 #include <concepts>
 #include <utility>
 
@@ -22,19 +18,19 @@ constexpr bool test() {
   // Check glvalue
   {
     int lvalue{};
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::ranges::__as_lvalue(lvalue);
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(lvalue);
   }
 
   // Check prvalue
   {
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::ranges::__as_lvalue(
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(
         0); // expected-warning {{temporary bound to local reference 'check' will be destroyed at the end of the full-expression}}
   }
 
   // Check xvalue
   {
     int xvalue{};
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::ranges::__as_lvalue(std::move(xvalue));
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(std::move(xvalue));
   }
 
   return true;

>From be16b577a981cc96f2eaa00024ecb341c6219085 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 16:12:40 +0100
Subject: [PATCH 03/18] Split `as-lvalue.verify.cpp`

Comments:
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396565383
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396565996
---
 .../as-lvalue.lifetimebound.verify.cpp        | 22 +++++++++++++++++++
 ...s-lvalue.verify.cpp => as-lvalue.pass.cpp} |  6 -----
 2 files changed, 22 insertions(+), 6 deletions(-)
 create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.lifetimebound.verify.cpp
 rename libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/{as-lvalue.verify.cpp => as-lvalue.pass.cpp} (79%)

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.lifetimebound.verify.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.lifetimebound.verify.cpp
new file mode 100644
index 0000000000000..7046936b1b7a7
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.lifetimebound.verify.cpp
@@ -0,0 +1,22 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+
+// template<class T>
+// constexpr T& as-lvalue(T&& t) { // exposition only
+
+#include <utility>
+
+void test() {
+  // Check prvalue
+  {
+    [[maybe_unused]] auto& check = std::__as_lvalue(
+        0); // expected-warning {{temporary bound to local reference 'check' will be destroyed at the end of the full-expression}}
+  }
+}
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
similarity index 79%
rename from libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
rename to libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
index 8efd7e5ccf983..14a3db6367ddc 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.verify.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
@@ -21,12 +21,6 @@ constexpr bool test() {
     [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(lvalue);
   }
 
-  // Check prvalue
-  {
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(
-        0); // expected-warning {{temporary bound to local reference 'check' will be destroyed at the end of the full-expression}}
-  }
-
   // Check xvalue
   {
     int xvalue{};

>From 181cd853f50801d7ea5ecf232ae3413ac10f4c83 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 16:20:24 +0100
Subject: [PATCH 04/18] Add missing `explicit` specifier in
 `__iterator(_Parent& __parent)`

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396574310
---
 libcxx/include/__ranges/join_view.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index fd68358837e0f..909ca9a06d3ad 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -265,7 +265,7 @@ namespace ranges {
       __satisfy();
     }
 
-    _LIBCPP_HIDE_FROM_ABI constexpr __iterator(_Parent& __parent)
+    _LIBCPP_HIDE_FROM_ABI constexpr explicit __iterator(_Parent& __parent)
       requires(!forward_range<_Base>)
         : __parent_(std::addressof(__parent)) {
       __satisfy();

>From d11d81cebf4fd96aa3b31e0908930e1f693d5c4d Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 17:22:35 +0100
Subject: [PATCH 05/18] Restore old `__sentinel<C>` friendship in
 `join_view::_sentinel`

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396587214
---
 libcxx/include/__ranges/join_view.h | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index 909ca9a06d3ad..e1d54c4e4bbc5 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -164,10 +164,11 @@ namespace ranges {
   template<bool _Const>
   struct join_view<_View>::__sentinel {
   private:
-    friend join_view;
+    template <bool>
+    friend struct __sentinel;
 
-    using _Parent = __maybe_const<_Const, join_view>;
-    using _Base = __maybe_const<_Const, _View>;
+    using _Parent            = __maybe_const<_Const, join_view>;
+    using _Base              = __maybe_const<_Const, _View>;
     sentinel_t<_Base> __end_ = sentinel_t<_Base>();
 
   public:

>From 03454c21dade534913e91ef137d2a16f2031a618 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 18:16:30 +0100
Subject: [PATCH 06/18] Un-experimental more `join_view` related tests

---
 .../alg.copy/ranges.copy_backward.segmented.pass.cpp            | 2 --
 .../alg.copy/ranges.copy_n.segmented.pass.cpp                   | 2 --
 .../alg.move/ranges.move.segmented.pass.cpp                     | 2 --
 .../alg.move/ranges.move_backward.segmented.pass.cpp            | 2 --
 .../conventions/customization.point.object/cpo.compile.pass.cpp | 2 --
 .../std/ranges/iterator_robust_against_adl.compile.pass.cpp     | 1 -
 6 files changed, 11 deletions(-)

diff --git a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.segmented.pass.cpp b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.segmented.pass.cpp
index c434cea1208cf..efeada5762558 100644
--- a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.segmented.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_backward.segmented.pass.cpp
@@ -7,8 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 
 #include <algorithm>
 #include <array>
diff --git a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_n.segmented.pass.cpp b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_n.segmented.pass.cpp
index eae40cefa663f..7da0f30775905 100644
--- a/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_n.segmented.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.modifying.operations/alg.copy/ranges.copy_n.segmented.pass.cpp
@@ -7,8 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 
 #include <algorithm>
 #include <array>
diff --git a/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move.segmented.pass.cpp b/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move.segmented.pass.cpp
index 2df6a10b18504..e29ba8af07d6f 100644
--- a/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move.segmented.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move.segmented.pass.cpp
@@ -7,8 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 
 #include <algorithm>
 #include <array>
diff --git a/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move_backward.segmented.pass.cpp b/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move_backward.segmented.pass.cpp
index 0f0a71439a10d..50f371a6d64d3 100644
--- a/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move_backward.segmented.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.modifying.operations/alg.move/ranges.move_backward.segmented.pass.cpp
@@ -7,8 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 
 #include <algorithm>
 #include <array>
diff --git a/libcxx/test/std/library/description/conventions/customization.point.object/cpo.compile.pass.cpp b/libcxx/test/std/library/description/conventions/customization.point.object/cpo.compile.pass.cpp
index e6c0e09dfff5f..060f179fe1683 100644
--- a/libcxx/test/std/library/description/conventions/customization.point.object/cpo.compile.pass.cpp
+++ b/libcxx/test/std/library/description/conventions/customization.point.object/cpo.compile.pass.cpp
@@ -7,8 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// TODO: make `join_view` non-experimental once D2770 is implemented.
-// UNSUPPORTED: !c++experimental
 
 // [customization.point.object]
 // [range.adaptor.object] "A range adaptor object is a customization point object..."
diff --git a/libcxx/test/std/ranges/iterator_robust_against_adl.compile.pass.cpp b/libcxx/test/std/ranges/iterator_robust_against_adl.compile.pass.cpp
index 09b77c0901a22..5efd6c72a13db 100644
--- a/libcxx/test/std/ranges/iterator_robust_against_adl.compile.pass.cpp
+++ b/libcxx/test/std/ranges/iterator_robust_against_adl.compile.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: !c++experimental
 
 // ADL call with nested iterators of views should not look up base's view's
 // namespace

>From 7de9a775a69fdf5496e3d73be9592ae768e9ce40 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 21:21:06 +0100
Subject: [PATCH 07/18] Make `join_view::__iterator::__outer_` private

Which is hopefully enough.

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396575242
---
 libcxx/include/__ranges/join_view.h | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index e1d54c4e4bbc5..e75e61270721c 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -219,12 +219,9 @@ namespace ranges {
     static constexpr bool _OuterPresent = forward_range<_Base>;
     using _OuterType                    = _If<_OuterPresent, _Outer, std::__empty>;
 
-  public:
     _LIBCPP_NO_UNIQUE_ADDRESS _OuterType __outer_ = _OuterType();
-
-  private:
     optional<_Inner> __inner_;
-    _Parent *__parent_ = nullptr;
+    _Parent* __parent_ = nullptr;
 
     _LIBCPP_HIDE_FROM_ABI
     constexpr void __satisfy() {
@@ -243,7 +240,7 @@ namespace ranges {
       if constexpr (__ref_is_glvalue)
         __inner_.reset();
     }
-
+    
     _LIBCPP_HIDE_FROM_ABI constexpr _Outer& __get_outer() {
       if constexpr (forward_range<_Base>) {
         return __outer_;

>From f256e1f9e663ec64ade659520b1b9494d8f0fe9d Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 21:45:52 +0100
Subject: [PATCH 08/18] Add test with code from LWG-3698

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396600533
---
 .../range.join/lwg3698.pass.cpp               | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp

diff --git a/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp
new file mode 100644
index 0000000000000..c1e81aef5f8e5
--- /dev/null
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-localization
+
+// Check LWG-3698: `regex_iterator` and `join_view` don't work together very well
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <ranges>
+#include <regex>
+#include <string_view>
+
+int main(int, char**) {
+  char const text[] = "Hello";
+  std::regex regex{"[a-z]"};
+
+  auto lower =
+      std::ranges::subrange(
+          std::cregex_iterator(std::ranges::begin(text), std::ranges::end(text), regex), std::cregex_iterator{}) |
+      std::views::join | std::views::transform([](auto const& sm) { return std::string_view(sm.first, sm.second); });
+
+  assert(std::ranges::equal(lower, std::to_array<std::string_view>({"e", "l", "l", "o"})));
+}

>From 1f00358e80414f42ff2b42249c7ed9fe5399e881 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 21:58:02 +0100
Subject: [PATCH 09/18] Whitespace amendments

---
 libcxx/include/__ranges/join_view.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index e75e61270721c..9b7399c24e3a5 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -218,8 +218,8 @@ namespace ranges {
 
     static constexpr bool _OuterPresent = forward_range<_Base>;
     using _OuterType                    = _If<_OuterPresent, _Outer, std::__empty>;
-
     _LIBCPP_NO_UNIQUE_ADDRESS _OuterType __outer_ = _OuterType();
+
     optional<_Inner> __inner_;
     _Parent* __parent_ = nullptr;
 
@@ -240,7 +240,7 @@ namespace ranges {
       if constexpr (__ref_is_glvalue)
         __inner_.reset();
     }
-    
+
     _LIBCPP_HIDE_FROM_ABI constexpr _Outer& __get_outer() {
       if constexpr (forward_range<_Base>) {
         return __outer_;

>From d5d061075d3b86104880fd3a4fd9ac35474612f8 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 22:11:36 +0100
Subject: [PATCH 10/18] Add extra static assertion for `[range.join.iterator]
 Note 1`

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396578912
---
 libcxx/include/__ranges/join_view.h | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/libcxx/include/__ranges/join_view.h b/libcxx/include/__ranges/join_view.h
index 9b7399c24e3a5..f80beda33b11e 100644
--- a/libcxx/include/__ranges/join_view.h
+++ b/libcxx/include/__ranges/join_view.h
@@ -214,6 +214,8 @@ namespace ranges {
     using _Inner = iterator_t<range_reference_t<_Base>>;
     using _InnerRange = range_reference_t<_View>;
 
+    static_assert(!_Const || forward_range<_Base>, "Const can only be true when Base models forward_range.");
+
     static constexpr bool __ref_is_glvalue = is_reference_v<range_reference_t<_Base>>;
 
     static constexpr bool _OuterPresent = forward_range<_Base>;

>From 3861ffa29062879070bc6acea0dde8fff036aba2 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Mon, 27 Nov 2023 22:36:12 +0100
Subject: [PATCH 11/18] Fix comment: `!forward_range<iterator_t<Base>>` ->
 `!forward_range<Base>`

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396594058
---
 .../range.adaptors/range.join/range.join.iterator/eq.pass.cpp   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
index 8b7e694b080f4..5c831f33e67c7 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/range.join.iterator/eq.pass.cpp
@@ -42,7 +42,7 @@ constexpr bool test() {
   }
 
   {
-    // !forward_range<iterator_t<Base>>
+    // !forward_range<Base>
     using Inner = BufferView<int*>;
     using Outer = BufferView<cpp20_input_iterator<Inner*>, sentinel_wrapper<cpp20_input_iterator<Inner*>>>;
     static_assert(!std::equality_comparable<std::ranges::iterator_t<Outer>>);

>From 0e927818e45d361e3fdc76df9f5938ea291f7bf1 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Tue, 28 Nov 2023 00:53:35 +0100
Subject: [PATCH 12/18] Make `as-lvalue.pass.cpp` C++11 friendly

---
 .../range.adaptor.helpers/as-lvalue.pass.cpp  | 22 ++++++-------------
 1 file changed, 7 insertions(+), 15 deletions(-)

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
index 14a3db6367ddc..d5cd81f397749 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
@@ -11,28 +11,20 @@
 // template<class T>
 // constexpr T& as-lvalue(T&& t) { // exposition only
 
-#include <concepts>
+#include <cassert>
+#include <type_traits>
 #include <utility>
 
-constexpr bool test() {
-  // Check glvalue
-  {
-    int lvalue{};
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(lvalue);
-  }
+constexpr bool test(int value = 0) {
+  static_assert(std::is_same<decltype(std::__as_lvalue(value)), int&>::value, "");
+  static_assert(std::is_same<decltype(std::__as_lvalue(std::move(value))), int&>::value, "");
 
-  // Check xvalue
-  {
-    int xvalue{};
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(std::move(xvalue));
-  }
-
-  return true;
+  return (assert(&std::__as_lvalue(value) == &value), assert(&std::__as_lvalue(std::move(value)) == &value), true);
 }
 
 int main(int, char**) {
   test();
-  static_assert(test());
+  static_assert(test(), "");
 
   return 0;
 }

>From 54c31fc10fbf73839c4d1b1e434f6fd9cfca2ca7 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Wed, 29 Nov 2023 17:47:07 +0100
Subject: [PATCH 13/18] Add `return 0;` in LWG-3698 test

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1409553823
---
 .../test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp  | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp b/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp
index c1e81aef5f8e5..0abe37bf17f7e 100644
--- a/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp
+++ b/libcxx/test/std/ranges/range.adaptors/range.join/lwg3698.pass.cpp
@@ -28,4 +28,6 @@ int main(int, char**) {
       std::views::join | std::views::transform([](auto const& sm) { return std::string_view(sm.first, sm.second); });
 
   assert(std::ranges::equal(lower, std::to_array<std::string_view>({"e", "l", "l", "o"})));
+
+  return 0;
 }

>From 62fa3a7d86b63459dde492711040c11a5f6bc4c5 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Wed, 29 Nov 2023 17:48:07 +0100
Subject: [PATCH 14/18] Revert "Make `as-lvalue.pass.cpp` C++11 friendly"

This reverts commit 0e927818e45d361e3fdc76df9f5938ea291f7bf1.
---
 .../range.adaptor.helpers/as-lvalue.pass.cpp  | 22 +++++++++++++------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
index d5cd81f397749..14a3db6367ddc 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
@@ -11,20 +11,28 @@
 // template<class T>
 // constexpr T& as-lvalue(T&& t) { // exposition only
 
-#include <cassert>
-#include <type_traits>
+#include <concepts>
 #include <utility>
 
-constexpr bool test(int value = 0) {
-  static_assert(std::is_same<decltype(std::__as_lvalue(value)), int&>::value, "");
-  static_assert(std::is_same<decltype(std::__as_lvalue(std::move(value))), int&>::value, "");
+constexpr bool test() {
+  // Check glvalue
+  {
+    int lvalue{};
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(lvalue);
+  }
 
-  return (assert(&std::__as_lvalue(value) == &value), assert(&std::__as_lvalue(std::move(value)) == &value), true);
+  // Check xvalue
+  {
+    int xvalue{};
+    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(std::move(xvalue));
+  }
+
+  return true;
 }
 
 int main(int, char**) {
   test();
-  static_assert(test(), "");
+  static_assert(test());
 
   return 0;
 }

>From aebbbac53d1b3a1537e04610c3ed7d7436676e76 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Wed, 29 Nov 2023 17:48:40 +0100
Subject: [PATCH 15/18] Make `as-lvalue.pass.cpp` C++14 friendly

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1409545297
---
 .../range.adaptor.helpers/as-lvalue.pass.cpp         | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
index 14a3db6367ddc..721279fcd586b 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.adaptor.helpers/as-lvalue.pass.cpp
@@ -6,25 +6,27 @@
 //
 //===----------------------------------------------------------------------===//
 
-// UNSUPPORTED: c++03
+// UNSUPPORTED: c++03, c++11
 
 // template<class T>
 // constexpr T& as-lvalue(T&& t) { // exposition only
 
-#include <concepts>
+#include <type_traits>
 #include <utility>
 
 constexpr bool test() {
   // Check glvalue
   {
     int lvalue{};
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(lvalue);
+    [[maybe_unused]] decltype(auto) check = std::__as_lvalue(lvalue);
+    static_assert(std::is_same<decltype(check), int&>::value, "");
   }
 
   // Check xvalue
   {
     int xvalue{};
-    [[maybe_unused]] std::same_as<int&> decltype(auto) check = std::__as_lvalue(std::move(xvalue));
+    [[maybe_unused]] decltype(auto) check = std::__as_lvalue(std::move(xvalue));
+    static_assert(std::is_same<decltype(check), int&>::value, "");
   }
 
   return true;
@@ -32,7 +34,7 @@ constexpr bool test() {
 
 int main(int, char**) {
   test();
-  static_assert(test());
+  static_assert(test(), "");
 
   return 0;
 }

>From ca61a7060e4f0004da00fabd2ace58f8d523ccff Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Tue, 5 Dec 2023 12:15:42 +0100
Subject: [PATCH 16/18] Test `iterator(Parent& parent, OuterIter outer)`
 constructor

Comments:
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396589173
* https://github.com/llvm/llvm-project/pull/66033#discussion_r1396590292 (partially)
---
 .../ctor.parent.outer.pass.cpp                |  65 +++++++++++
 .../range.join/range.join.iterator/types.h    | 109 ++++++++++++++++++
 2 files changed, 174 insertions(+)
 create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp
 create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp
new file mode 100644
index 0000000000000..9bf55f0a3423a
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.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
+
+// constexpr iterator(Parent& parent, OuterIter outer)
+//   requires forward_range<Base>; // exposition only
+
+#include <cassert>
+#include <ranges>
+#include <string>
+#include <utility>
+
+#include "types.h"
+
+constexpr bool test() {
+  std::string strings[4] = {"aaaa", "bbbb", "cccc", "dddd"};
+
+  { // Check if `outer_` is initialized with `std::move(outer)` for `iterator<false>`
+    MoveOnAccessSubrange r(DieOnCopyIterator(strings), sentinel_wrapper(strings + 4));
+    std::ranges::join_view jv(std::move(r));
+    auto iter = jv.begin(); // Calls `iterator(Parent& parent, OuterIter outer)`
+    assert(*iter == 'a');
+  }
+
+  { // Check if `outer_` is initialized with `std::move(outer)` for `iterator<true>`
+    MoveOnAccessSubrange r(DieOnCopyIterator(strings), sentinel_wrapper(strings + 4));
+    std::ranges::join_view jv(std::ranges::ref_view{r});
+    auto iter = std::as_const(jv).begin(); // Calls `iterator(Parent& parent, OuterIter outer)`
+    assert(*iter == 'a');
+  }
+
+  {
+    // LWG3569 Inner iterator not default_initializable
+    // With the current spec, the constructor under test invokes Inner iterator's default constructor
+    // even if it is not default constructible.
+    // This test is checking that this constructor can be invoked with an inner range with non default
+    // constructible iterator.
+    using NonDefaultCtrIter = cpp20_input_iterator<int*>;
+    static_assert(!std::default_initializable<NonDefaultCtrIter>);
+    using NonDefaultCtrIterView = BufferView<NonDefaultCtrIter, sentinel_wrapper<NonDefaultCtrIter>>;
+    static_assert(std::ranges::input_range<NonDefaultCtrIterView>);
+
+    int buffer[2][2]               = {{1, 2}, {3, 4}};
+    NonDefaultCtrIterView inners[] = {buffer[0], buffer[1]};
+    auto outer                     = std::views::all(inners);
+    std::ranges::join_view jv(outer);
+    auto iter = jv.begin(); // Calls `iterator(Parent& parent, OuterIter outer)`
+    assert(*iter == 1);
+  }
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h
new file mode 100644
index 0000000000000..a5b88ed01773c
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h
@@ -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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef TEST_LIBCXX_RANGES_RANGE_ADAPTORS_RANGE_JOIN_RANGE_JOIN_ITERATOR_TYPES_H
+#define TEST_LIBCXX_RANGES_RANGE_ADAPTORS_RANGE_JOIN_RANGE_JOIN_ITERATOR_TYPES_H
+
+#include <cassert>
+#include <cstddef>
+#include <ranges>
+
+#include "test_iterators.h"
+
+template <std::forward_iterator Iter>
+struct DieOnCopyIterator {
+  using value_type      = std::iter_value_t<Iter>;
+  using difference_type = std::iter_difference_t<Iter>;
+
+  DieOnCopyIterator() = default;
+  constexpr explicit DieOnCopyIterator(Iter iter) : iter_(std::move(iter)) {}
+  constexpr DieOnCopyIterator(DieOnCopyIterator&& other) = default;
+  DieOnCopyIterator& operator=(DieOnCopyIterator&&)      = default;
+
+  constexpr DieOnCopyIterator(const DieOnCopyIterator&) { assert(false); }
+  constexpr DieOnCopyIterator& operator=(const DieOnCopyIterator&) { assert(false); }
+
+  constexpr DieOnCopyIterator& operator++() {
+    ++iter_;
+    return *this;
+  }
+
+  constexpr void operator++(int) { iter_++; }
+
+  constexpr DieOnCopyIterator operator++(int)
+    requires std::forward_iterator<Iter>
+  {
+    auto tmp = *this;
+    ++tmp;
+    return tmp;
+  }
+
+  constexpr decltype(auto) operator*() const { return *iter_; }
+
+  friend constexpr bool operator==(const DieOnCopyIterator& left, const DieOnCopyIterator& right)
+    requires std::equality_comparable<Iter>
+  {
+    return left.iter_ == right.iter_;
+  }
+
+  friend constexpr bool operator==(const DieOnCopyIterator& it, const sentinel_wrapper<Iter>& se) {
+    return it.iter_ == se;
+  }
+
+private:
+  Iter iter_ = Iter();
+};
+
+template <class Iter>
+explicit DieOnCopyIterator(Iter) -> DieOnCopyIterator<Iter>;
+
+static_assert(std::forward_iterator<DieOnCopyIterator<int*>>);
+static_assert(!std::bidirectional_iterator<DieOnCopyIterator<int*>>);
+static_assert(std::sentinel_for<sentinel_wrapper<int*>, DieOnCopyIterator<int*>>);
+
+template <std::input_iterator Iter, std::sentinel_for<Iter> Sent = Iter>
+struct MoveOnAccessSubrange : std::ranges::view_base {
+  constexpr explicit MoveOnAccessSubrange(Iter iter, Sent sent) : iter_(std::move(iter)), sent_(std::move(sent)) {}
+
+  MoveOnAccessSubrange(MoveOnAccessSubrange&&)            = default;
+  MoveOnAccessSubrange& operator=(MoveOnAccessSubrange&&) = default;
+
+  MoveOnAccessSubrange(const MoveOnAccessSubrange&)            = delete;
+  MoveOnAccessSubrange& operator=(const MoveOnAccessSubrange&) = delete;
+
+  constexpr Iter begin() { return std::move(iter_); }
+  constexpr Sent end() { return std::move(sent_); }
+
+private:
+  Iter iter_;
+  Sent sent_;
+};
+
+template <class Iter, class Sent>
+MoveOnAccessSubrange(Iter, Sent) -> MoveOnAccessSubrange<Iter, Sent>;
+
+static_assert(std::ranges::input_range<MoveOnAccessSubrange<int*, sentinel_wrapper<int*>>>);
+static_assert(std::ranges::forward_range<MoveOnAccessSubrange<DieOnCopyIterator<int*>>>);
+
+template <class Iter, class Sent>
+  requires(!std::same_as<Iter, Sent>)
+struct BufferView : std::ranges::view_base {
+  using T = std::iter_value_t<Iter>;
+  T* data_;
+  std::size_t size_;
+
+  template <std::size_t N>
+  constexpr BufferView(T (&b)[N]) : data_(b), size_(N) {}
+
+  constexpr Iter begin() const { return Iter(data_); }
+  constexpr Sent end() const { return Sent(Iter(data_ + size_)); }
+};
+
+static_assert(std::ranges::input_range<BufferView<int*, sentinel_wrapper<int*>>>);
+
+#endif // TEST_LIBCXX_RANGES_RANGE_ADAPTORS_RANGE_JOIN_RANGE_JOIN_ITERATOR_TYPES_H

>From 024b9e240cf19316c36712784f927e4b8e0dff8a Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Tue, 5 Dec 2023 12:34:46 +0100
Subject: [PATCH 17/18] Test `iterator(Parent& parent)` constructor

Comment: https://github.com/llvm/llvm-project/pull/66033#discussion_r1396590292
---
 .../range.join.iterator/ctor.parent.pass.cpp  | 36 +++++++++++++++++++
 .../range.join/range.join.iterator/types.h    |  9 +++--
 2 files changed, 43 insertions(+), 2 deletions(-)
 create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp
new file mode 100644
index 0000000000000..bb8debb642b99
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp
@@ -0,0 +1,36 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+
+// constexpr explicit iterator(Parent& parent)
+//   requires (!forward_range<Base>); // exposition only
+
+#include <string>
+#include <ranges>
+
+#include "types.h"
+
+constexpr bool test() {
+  std::string strings[4] = {"eeee", "ffff", "gggg", "hhhh"};
+
+  MoveOnAccessSubrange r(
+      DieOnCopyIterator(cpp20_input_iterator(strings)), sentinel_wrapper(cpp20_input_iterator(strings + 4)));
+  std::ranges::join_view jv(std::move(r));
+  auto iter = jv.begin(); // Calls `iterator(Parent& parent)`
+  assert(*iter == 'e');
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+
+  return 0;
+}
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h
index a5b88ed01773c..0652d4bdb4717 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/types.h
@@ -15,12 +15,15 @@
 
 #include "test_iterators.h"
 
-template <std::forward_iterator Iter>
+template <std::input_iterator Iter>
 struct DieOnCopyIterator {
   using value_type      = std::iter_value_t<Iter>;
   using difference_type = std::iter_difference_t<Iter>;
 
-  DieOnCopyIterator() = default;
+  DieOnCopyIterator()
+    requires std::default_initializable<Iter>
+  = default;
+
   constexpr explicit DieOnCopyIterator(Iter iter) : iter_(std::move(iter)) {}
   constexpr DieOnCopyIterator(DieOnCopyIterator&& other) = default;
   DieOnCopyIterator& operator=(DieOnCopyIterator&&)      = default;
@@ -62,6 +65,8 @@ struct DieOnCopyIterator {
 template <class Iter>
 explicit DieOnCopyIterator(Iter) -> DieOnCopyIterator<Iter>;
 
+static_assert(std::input_iterator<DieOnCopyIterator<cpp20_input_iterator<int*>>>);
+static_assert(!std::forward_iterator<DieOnCopyIterator<cpp20_input_iterator<int*>>>);
 static_assert(std::forward_iterator<DieOnCopyIterator<int*>>);
 static_assert(!std::bidirectional_iterator<DieOnCopyIterator<int*>>);
 static_assert(std::sentinel_for<sentinel_wrapper<int*>, DieOnCopyIterator<int*>>);

>From 710398a5f10f949eab07ef4e2767f3619bad5e61 Mon Sep 17 00:00:00 2001
From: Jakub Mazurkiewicz <mazkuba3 at gmail.com>
Date: Tue, 5 Dec 2023 13:24:04 +0100
Subject: [PATCH 18/18] Address GCC's complaints

---
 .../range.join/range.join.iterator/ctor.parent.outer.pass.cpp | 4 ++--
 .../range.join/range.join.iterator/ctor.parent.pass.cpp       | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp
index 9bf55f0a3423a..a54980bec287f 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.outer.pass.cpp
@@ -22,14 +22,14 @@ constexpr bool test() {
   std::string strings[4] = {"aaaa", "bbbb", "cccc", "dddd"};
 
   { // Check if `outer_` is initialized with `std::move(outer)` for `iterator<false>`
-    MoveOnAccessSubrange r(DieOnCopyIterator(strings), sentinel_wrapper(strings + 4));
+    MoveOnAccessSubrange r{DieOnCopyIterator(strings), sentinel_wrapper(strings + 4)};
     std::ranges::join_view jv(std::move(r));
     auto iter = jv.begin(); // Calls `iterator(Parent& parent, OuterIter outer)`
     assert(*iter == 'a');
   }
 
   { // Check if `outer_` is initialized with `std::move(outer)` for `iterator<true>`
-    MoveOnAccessSubrange r(DieOnCopyIterator(strings), sentinel_wrapper(strings + 4));
+    MoveOnAccessSubrange r{DieOnCopyIterator(strings), sentinel_wrapper(strings + 4)};
     std::ranges::join_view jv(std::ranges::ref_view{r});
     auto iter = std::as_const(jv).begin(); // Calls `iterator(Parent& parent, OuterIter outer)`
     assert(*iter == 'a');
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp
index bb8debb642b99..3026a02abf00f 100644
--- a/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.join/range.join.iterator/ctor.parent.pass.cpp
@@ -19,8 +19,8 @@
 constexpr bool test() {
   std::string strings[4] = {"eeee", "ffff", "gggg", "hhhh"};
 
-  MoveOnAccessSubrange r(
-      DieOnCopyIterator(cpp20_input_iterator(strings)), sentinel_wrapper(cpp20_input_iterator(strings + 4)));
+  MoveOnAccessSubrange r{
+      DieOnCopyIterator(cpp20_input_iterator(strings)), sentinel_wrapper(cpp20_input_iterator(strings + 4))};
   std::ranges::join_view jv(std::move(r));
   auto iter = jv.begin(); // Calls `iterator(Parent& parent)`
   assert(*iter == 'e');



More information about the llvm-commits mailing list