https://gcc.gnu.org/g:3836df7e895beda1f159620bfd20024136fda9f0

commit r15-2446-g3836df7e895beda1f159620bfd20024136fda9f0
Author: Jonathan Wakely <jwak...@redhat.com>
Date:   Tue Jul 9 12:12:56 2024 +0100

    libstdc++: Implement C++26 type checking for std::format args [PR115776]
    
    Implement the changes from P2757R3, which enhance the parse context to
    be able to do type checking on format arguments, and to use that to
    ensure that args used for width and precisions are integral types.
    
    libstdc++-v3/ChangeLog:
    
            PR libstdc++/115776
            * include/bits/version.def (format): Update for C++26.
            * include/bits/version.h: Regenerate.
            * include/std/format (basic_format_parse_context): Remove
            default argument from constructor and split into two
            constructors. Make the constructor taking size_t private for
            C++26 and later.
            (basic_format_parse_context::check_dynamic_spec): New member
            function template.
            (basic_format_parse_context::check_dynamic_spec_integral): New
            member function.
            (basic_format_parse_context::check_dynamic_spec_string):
            Likewise.
            (__format::_Spec::_S_parse_width_or_precision): Use
            check_dynamic_spec_integral.
            (__format::__to_arg_t_enum): New helper function.
            (basic_format_arg):  Declare __to_arg_t_enum as friend.
            (__format::_Scanner): Define and use a derived parse context
            type.
            (__format::_Checking_scanner): Make arg types available to parse
            context.
            * testsuite/std/format/functions/format.cc: Check for new values
            of __cpp_lib_format macro.
            * testsuite/std/format/parse_ctx.cc: Check all members of
            basic_format_parse_context.
            * testsuite/std/format/parse_ctx_neg.cc: New test.
            * testsuite/std/format/string.cc: Add more checks for dynamic
            width and precision args.

Diff:
---
 libstdc++-v3/include/bits/version.def              |  10 +-
 libstdc++-v3/include/bits/version.h                |   7 +-
 libstdc++-v3/include/std/format                    | 142 +++++++++++++++++++-
 .../testsuite/std/format/functions/format.cc       |   4 +
 libstdc++-v3/testsuite/std/format/parse_ctx.cc     | 145 +++++++++++++++++++++
 libstdc++-v3/testsuite/std/format/parse_ctx_neg.cc |  39 ++++++
 libstdc++-v3/testsuite/std/format/string.cc        |  13 ++
 7 files changed, 347 insertions(+), 13 deletions(-)

diff --git a/libstdc++-v3/include/bits/version.def 
b/libstdc++-v3/include/bits/version.def
index 1acc9cd5cb9a..bcb33c18aa4d 100644
--- a/libstdc++-v3/include/bits/version.def
+++ b/libstdc++-v3/include/bits/version.def
@@ -1165,11 +1165,11 @@ ftms = {
   // 202305 P2757R3 Type checking format args
   // 202306 P2637R3 Member visit
   // 202311 P2918R2 Runtime format strings II
-  // values = {
-    // v = 202305;
-    // cxxmin = 26;
-    // hosted = yes;
-  // };
+  values = {
+    v = 202305;
+    cxxmin = 26;
+    hosted = yes;
+  };
   // 201907 Text Formatting, Integration of chrono, printf corner cases.
   // 202106 std::format improvements.
   // 202110 Fixing locale handling in chrono formatters, generator-like types.
diff --git a/libstdc++-v3/include/bits/version.h 
b/libstdc++-v3/include/bits/version.h
index 5cd77770e213..4d1af34bf8d5 100644
--- a/libstdc++-v3/include/bits/version.h
+++ b/libstdc++-v3/include/bits/version.h
@@ -1304,7 +1304,12 @@
 #undef __glibcxx_want_barrier
 
 #if !defined(__cpp_lib_format)
-# if (__cplusplus >= 202002L) && _GLIBCXX_HOSTED
+# if (__cplusplus >  202302L) && _GLIBCXX_HOSTED
+#  define __glibcxx_format 202305L
+#  if defined(__glibcxx_want_all) || defined(__glibcxx_want_format)
+#   define __cpp_lib_format 202305L
+#  endif
+# elif (__cplusplus >= 202002L) && _GLIBCXX_HOSTED
 #  define __glibcxx_format 202304L
 #  if defined(__glibcxx_want_all) || defined(__glibcxx_want_format)
 #   define __cpp_lib_format 202304L
diff --git a/libstdc++-v3/include/std/format b/libstdc++-v3/include/std/format
index 2669ad8c2440..6a88705ec7b9 100644
--- a/libstdc++-v3/include/std/format
+++ b/libstdc++-v3/include/std/format
@@ -222,6 +222,9 @@ namespace __format
   inline void
   __failed_to_parse_format_spec()
   { __throw_format_error("format error: failed to parse format-spec"); }
+
+  template<typename _CharT> class _Scanner;
+
 } // namespace __format
   /// @endcond
 
@@ -241,9 +244,8 @@ namespace __format
       using iterator = const_iterator;
 
       constexpr explicit
-      basic_format_parse_context(basic_string_view<_CharT> __fmt,
-                                size_t __num_args = 0) noexcept
-      : _M_begin(__fmt.begin()), _M_end(__fmt.end()), _M_num_args(__num_args)
+      basic_format_parse_context(basic_string_view<_CharT> __fmt) noexcept
+      : _M_begin(__fmt.begin()), _M_end(__fmt.end())
       { }
 
       basic_format_parse_context(const basic_format_parse_context&) = delete;
@@ -283,13 +285,78 @@ namespace __format
            __format::__invalid_arg_id_in_format_string();
       }
 
+#if __cpp_lib_format >= 202305L
+      template<typename... _Ts>
+       constexpr void
+       check_dynamic_spec(size_t __id) noexcept;
+
+      constexpr void
+      check_dynamic_spec_integral(size_t __id) noexcept
+      {
+       check_dynamic_spec<int, unsigned, long long, unsigned long long>(__id);
+      }
+
+      constexpr void
+      check_dynamic_spec_string(size_t __id) noexcept
+      {
+       check_dynamic_spec<const char_type*, basic_string_view<_CharT>>(__id);
+      }
+
+    private:
+      // Check the Mandates: condition for check_dynamic_spec<Ts...>(n)
+      template<typename... _Ts>
+       static consteval bool
+       __check_dynamic_spec_types()
+       {
+         if constexpr (sizeof...(_Ts))
+           {
+             int __counts[] = {
+               (is_same_v<bool, _Ts> + ...),
+               (is_same_v<_CharT, _Ts> + ...),
+               (is_same_v<int, _Ts> + ...),
+               (is_same_v<unsigned, _Ts> + ...),
+               (is_same_v<long long, _Ts> + ...),
+               (is_same_v<unsigned long long, _Ts> + ...),
+               (is_same_v<float, _Ts> + ...),
+               (is_same_v<double, _Ts> + ...),
+               (is_same_v<long double, _Ts> + ...),
+               (is_same_v<const _CharT*, _Ts> + ...),
+               (is_same_v<basic_string_view<_CharT>, _Ts> + ...),
+               (is_same_v<const void*, _Ts> + ...)
+             };
+             int __sum = 0;
+             for (int __c : __counts)
+               {
+                 __sum += __c;
+                 if (__c > 1)
+                   __invalid_dynamic_spec("non-unique template argument type");
+               }
+             if (__sum != sizeof...(_Ts))
+               __invalid_dynamic_spec("disallowed template argument type");
+           }
+         return true;
+       }
+
+      // This must not be constexpr.
+      static void __invalid_dynamic_spec(const char*);
+
+      friend __format::_Scanner<_CharT>;
+#endif
+
+      // This constructor should only be used by the implementation.
+      constexpr explicit
+      basic_format_parse_context(basic_string_view<_CharT> __fmt,
+                                size_t __num_args) noexcept
+      : _M_begin(__fmt.begin()), _M_end(__fmt.end()), _M_num_args(__num_args)
+      { }
+
     private:
       iterator _M_begin;
       iterator _M_end;
       enum _Indexing { _Unknown, _Manual, _Auto };
       _Indexing _M_indexing = _Unknown;
       size_t _M_next_arg_id = 0;
-      size_t _M_num_args;
+      size_t _M_num_args = 0;
     };
 
 /// @cond undocumented
@@ -549,6 +616,9 @@ namespace __format
                __pc.check_arg_id(__v);
                __val = __v;
              }
+#if __cpp_lib_format >= 202305L
+           __pc.check_dynamic_spec_integral(__val);
+#endif
            ++__first; // past the '}'
          }
        return __first;
@@ -3205,6 +3275,10 @@ namespace __format
   template<typename _Context, typename... _Args>
     class _Arg_store;
 
+  template<typename _Ch, typename _Tp>
+    consteval _Arg_t
+    __to_arg_t_enum() noexcept;
+
 } // namespace __format
 /// @endcond
 
@@ -3486,6 +3560,10 @@ namespace __format
        friend decltype(auto)
        visit_format_arg(_Visitor&& __vis, basic_format_arg<_Ctx>);
 
+      template<typename _Ch, typename _Tp>
+       friend consteval __format::_Arg_t
+       __format::__to_arg_t_enum() noexcept;
+
       template<typename _Visitor>
        decltype(auto)
        _M_visit(_Visitor&& __vis, __format::_Arg_t __type)
@@ -3899,7 +3977,11 @@ namespace __format
     {
       using iterator = typename basic_format_parse_context<_CharT>::iterator;
 
-      basic_format_parse_context<_CharT> _M_pc;
+      struct _Parse_context : basic_format_parse_context<_CharT>
+      {
+       using basic_format_parse_context<_CharT>::basic_format_parse_context;
+       const _Arg_t* _M_types = nullptr;
+      } _M_pc;
 
       constexpr explicit
       _Scanner(basic_string_view<_CharT> __str, size_t __nargs = -1)
@@ -4059,6 +4141,16 @@ namespace __format
       }
     };
 
+  template<typename _CharT, typename _Tp>
+    consteval _Arg_t
+    __to_arg_t_enum() noexcept
+    {
+      using _Context = __format::__format_context<_CharT>;
+      using _Fmt_arg = basic_format_arg<_Context>;
+      using _NormalizedTp = typename _Fmt_arg::template _Normalize<_Tp>;
+      return _Fmt_arg::template _S_to_enum<_NormalizedTp>();
+    }
+
   // Validate a format string for Args.
   template<typename _CharT, typename... _Args>
     class _Checking_scanner : public _Scanner<_CharT>
@@ -4068,10 +4160,14 @@ namespace __format
        "std::formatter must be specialized for each type being formatted");
 
     public:
-      constexpr
+      consteval
       _Checking_scanner(basic_string_view<_CharT> __str)
       : _Scanner<_CharT>(__str, sizeof...(_Args))
-      { }
+      {
+#if __cpp_lib_format >= 202305L
+       this->_M_pc._M_types = _M_types.data();
+#endif
+      }
 
     private:
       constexpr void
@@ -4102,6 +4198,11 @@ namespace __format
          else
            __builtin_unreachable();
        }
+
+#if __cpp_lib_format >= 202305L
+      array<_Arg_t, sizeof...(_Args)>
+       _M_types{ { __format::__to_arg_t_enum<_CharT, _Args>()... } };
+#endif
     };
 
   template<typename _Out, typename _CharT, typename _Context>
@@ -4200,6 +4301,33 @@ namespace __format
 } // namespace __format
 /// @endcond
 
+#if __cpp_lib_format >= 202305L
+  template<typename _CharT>
+  template<typename... _Ts>
+    constexpr void
+    basic_format_parse_context<_CharT>::check_dynamic_spec(size_t __id) 
noexcept
+    {
+      constexpr bool __ok = __check_dynamic_spec_types<_Ts...>();
+
+      if consteval {
+       if (__id >= _M_num_args)
+         __format::__invalid_arg_id_in_format_string();
+       if constexpr (sizeof...(_Ts) != 0)
+         {
+           using _Parse_context = __format::_Scanner<_CharT>::_Parse_context;
+           auto __arg = static_cast<_Parse_context*>(this)->_M_types[__id];
+           __format::_Arg_t __types[] = {
+             __format::__to_arg_t_enum<_CharT, _Ts>()...
+           };
+           for (auto __t : __types)
+             if (__arg == __t)
+               return;
+         }
+       __invalid_dynamic_spec("arg(id) type does not match");
+      }
+    }
+#endif
+
   template<typename _CharT, typename... _Args>
     template<typename _Tp>
       requires convertible_to<const _Tp&, basic_string_view<_CharT>>
diff --git a/libstdc++-v3/testsuite/std/format/functions/format.cc 
b/libstdc++-v3/testsuite/std/format/functions/format.cc
index 5152bb0b0d06..3c441d711b3e 100644
--- a/libstdc++-v3/testsuite/std/format/functions/format.cc
+++ b/libstdc++-v3/testsuite/std/format/functions/format.cc
@@ -8,6 +8,8 @@
 # error "Feature test macro for std::format is missing in <format>"
 #elif __cpp_lib_format < 202110L
 # error "Feature test macro for std::format has wrong value in <format>"
+#elif __cplusplus > 202302L && __cpp_lib_format < 202305L
+# error "Feature test macro for std::format has wrong value in <format>"
 #endif
 
 #ifndef __cpp_lib_format_uchar
@@ -22,6 +24,8 @@
 # error "Feature test macro for std::format is missing in <version>"
 #elif __cpp_lib_format < 202110L
 # error "Feature test macro for std::format has wrong value in <version>"
+#elif __cplusplus > 202302L && __cpp_lib_format < 202305L
+# error "Feature test macro for std::format has wrong value in <version>"
 #endif
 
 #ifndef __cpp_lib_format_uchar
diff --git a/libstdc++-v3/testsuite/std/format/parse_ctx.cc 
b/libstdc++-v3/testsuite/std/format/parse_ctx.cc
index 3b3201c2a47d..b0cef2da41bc 100644
--- a/libstdc++-v3/testsuite/std/format/parse_ctx.cc
+++ b/libstdc++-v3/testsuite/std/format/parse_ctx.cc
@@ -3,6 +3,91 @@
 #include <format>
 #include <testsuite_hooks.h>
 
+static_assert(std::is_constructible_v<std::format_parse_context,
+                                     std::string_view>);
+static_assert(std::is_constructible_v<std::wformat_parse_context,
+                                     std::wstring_view>);
+
+#if __cpp_lib_format < 202305
+constexpr bool construct_with_num_args = true;
+#else
+constexpr bool construct_with_num_args = false;
+#endif
+
+static_assert(std::is_constructible_v<std::format_parse_context,
+                                     std::string_view, std::size_t>
+                                     == construct_with_num_args);
+static_assert(std::is_constructible_v<std::wformat_parse_context,
+                                     std::wstring_view, std::size_t>
+                                     == construct_with_num_args);
+
+static_assert( ! std::is_constructible_v<std::format_parse_context,
+                                        std::wstring_view>);
+static_assert( ! std::is_constructible_v<std::wformat_parse_context,
+                                        std::string_view>);
+
+static_assert( ! std::is_convertible_v<std::string_view,
+                                      std::format_parse_context> );
+static_assert( ! std::is_convertible_v<std::wstring_view,
+                                      std::wformat_parse_context> );
+
+static_assert( ! std::is_default_constructible_v<std::format_parse_context> );
+static_assert( ! std::is_copy_constructible_v<std::format_parse_context> );
+static_assert( ! std::is_move_constructible_v<std::format_parse_context> );
+static_assert( ! std::is_copy_assignable_v<std::format_parse_context> );
+static_assert( ! std::is_move_assignable_v<std::format_parse_context> );
+
+// This concept is satisfied if the next_arg_id() call is a constant expression
+template<typename Ch, typename PC = std::basic_format_parse_context<Ch>>
+concept arg_id_available = requires {
+  typename std::integral_constant<std::size_t,
+                                 PC({}).next_arg_id()>::type;
+};
+
+void
+test_members()
+{
+  std::string_view s = "spec string";
+
+  std::format_parse_context pc(s);
+
+  VERIFY( pc.begin() == s.begin() );
+  VERIFY( pc.end() == s.end() );
+  pc.advance_to(s.begin() + 5);
+  VERIFY( pc.begin() == s.begin() + 5 );
+
+  // Runtime calls to these do not check for the correct number of args.
+  VERIFY( pc.next_arg_id() == 0 );
+  VERIFY( pc.next_arg_id() == 1 );
+  VERIFY( pc.next_arg_id() == 2 );
+  try
+  {
+    // Cannot mix manual and automatic indexing.
+    pc.check_arg_id(0);
+    VERIFY( false );
+  }
+  catch (const std::format_error&)
+  {
+  }
+  // But they do check during constant evaluation:
+  VERIFY( ! arg_id_available<char> );
+  VERIFY( ! arg_id_available<wchar_t> );
+
+  std::format_parse_context pc2("");
+  pc2.check_arg_id(2);
+  pc2.check_arg_id(1);
+  pc2.check_arg_id(3);
+  try
+  {
+    // Cannot mix manual and automatic indexing.
+    (void) pc2.next_arg_id();
+    VERIFY( false );
+  }
+  catch (const std::format_error&)
+  {
+  }
+}
+
 template<typename T, bool auto_indexing = true>
 bool
 is_std_format_spec_for(std::string_view spec)
@@ -357,6 +442,65 @@ test_custom()
   VERIFY( ! is_std_format_spec_for<S>("") );
 }
 
+#if __cpp_lib_format >= 202305
+struct X { };
+
+template<>
+struct std::formatter<X, char>
+{
+  constexpr std::format_parse_context::iterator
+  parse(std::format_parse_context& pc)
+  {
+    std::string_view spec(pc.begin(), pc.end());
+    auto p = spec.find('}');
+    if (p != std::string_view::npos)
+      spec = spec.substr(0, p); // truncate to closing brace
+    if (spec == "int")
+    {
+      pc.check_dynamic_spec_integral(pc.next_arg_id());
+      integer = true;
+    }
+    else if (spec == "str")
+    {
+      pc.check_dynamic_spec_string(pc.next_arg_id());
+      integer = false;
+    }
+    else
+      throw std::format_error("invalid format-spec");
+    return pc.begin() + spec.size();
+  }
+
+  std::format_context::iterator
+  format(X, std::format_context& c) const
+  {
+    std::visit_format_arg([this]<typename T>(T) {
+      if (is_integral_v<T> != this->integer)
+       throw std::format_error("invalid argument type");
+    }, c.arg(1));
+    return c.out();
+  }
+private:
+  bool integer = false;
+};
+#endif
+
+void
+test_dynamic_type_check()
+{
+#if __cpp_lib_format >= 202305
+  std::format_parse_context pc("{1}.{2}");
+
+  // None of these calls should do anything at runtime, only during consteval:
+  pc.check_dynamic_spec<>(0);
+  pc.check_dynamic_spec<int, const char*>(0);
+  pc.check_dynamic_spec_integral(0);
+  pc.check_dynamic_spec_string(0);
+
+  (void) std::format("{:int}", X{}, 42L);
+  (void) std::format("{:str}", X{}, "H2G2");
+#endif
+}
+
 int main()
 {
   test_char();
@@ -366,4 +510,5 @@ int main()
   test_string();
   test_pointer();
   test_custom();
+  test_dynamic_type_check();
 }
diff --git a/libstdc++-v3/testsuite/std/format/parse_ctx_neg.cc 
b/libstdc++-v3/testsuite/std/format/parse_ctx_neg.cc
new file mode 100644
index 000000000000..d6a4366d7d0b
--- /dev/null
+++ b/libstdc++-v3/testsuite/std/format/parse_ctx_neg.cc
@@ -0,0 +1,39 @@
+// { dg-do compile { target c++26 } }
+
+#include <format>
+
+void
+test_invalid()
+{
+  std::format_parse_context pc("");
+
+  // These types are all valid:
+  pc.check_dynamic_spec<bool, char, int, unsigned, long long,
+                       unsigned long long, float, double, long double,
+                       const char*, std::string_view, const void*>(0);
+  // For some reason, an empty pack of types is valid:
+  pc.check_dynamic_spec<>(0);
+
+  pc.check_dynamic_spec<void>(0); // { dg-error "here" }
+  // const void* is allowed, but void* is not
+  pc.check_dynamic_spec<void*>(0); // { dg-error "here" }
+  // int and long long are allowed, but long is not
+  pc.check_dynamic_spec<long>(0); // { dg-error "here" }
+  // char_type is allowed, but other character types are not
+  pc.check_dynamic_spec<wchar_t>(0); // { dg-error "here" }
+  pc.check_dynamic_spec<char8_t>(0); // { dg-error "here" }
+  // std::string_view is allowed, but std::string is not
+  pc.check_dynamic_spec<std::string>(0); // { dg-error "here" }
+  pc.check_dynamic_spec<int, bool, int>(0); // { dg-error "here" }
+
+  std::wformat_parse_context wpc(L"");
+  wpc.check_dynamic_spec<bool, wchar_t, int, unsigned, long long,
+                        unsigned long long, float, double, long double,
+                        const wchar_t*, std::wstring_view, const void*>(0);
+  wpc.check_dynamic_spec<char>(0); // { dg-error "here" }
+  wpc.check_dynamic_spec<char16_t>(0); // { dg-error "here" }
+  wpc.check_dynamic_spec<char32_t>(0); // { dg-error "here" }
+}
+
+// Each failure above will point to a call to this non-constexpr function:
+// { dg-error "__invalid_dynamic_spec" "" { target *-*-* } 0 }
diff --git a/libstdc++-v3/testsuite/std/format/string.cc 
b/libstdc++-v3/testsuite/std/format/string.cc
index ddb3c5625cd0..d5f305001395 100644
--- a/libstdc++-v3/testsuite/std/format/string.cc
+++ b/libstdc++-v3/testsuite/std/format/string.cc
@@ -109,9 +109,16 @@ test_format_spec()
   VERIFY( ! is_format_string_for("{:#?}", "str") );
   VERIFY( ! is_format_string_for("{:#?}", 'c') );
 
+  // The 0 option is not valid for charT and bool.
   VERIFY( ! is_format_string_for("{:0c}", 'c') );
   VERIFY( ! is_format_string_for("{:0s}", true) );
 
+  // Dynamic width arg must be an integer type.
+  VERIFY( ! is_format_string_for("{:{}d}", 1, 1.5) );
+  VERIFY( ! is_format_string_for("{:{}d}", 1, true) );
+  VERIFY( ! is_format_string_for("{:{}d}", 1, "str") );
+  VERIFY( ! is_format_string_for("{:{}d}", 1, nullptr) );
+
   // Precision only valid for string and floating-point types.
   VERIFY( ! is_format_string_for("{:.3d}", 1) );
   VERIFY( ! is_format_string_for("{:3.3d}", 1) );
@@ -119,6 +126,12 @@ test_format_spec()
   VERIFY( ! is_format_string_for("{:3.3s}", 'c') );
   VERIFY( ! is_format_string_for("{:3.3p}", nullptr) );
 
+  // Dynamic precision arg must be an integer type.
+  VERIFY( ! is_format_string_for("{:.{}f}", 1.0, 1.5) );
+  VERIFY( ! is_format_string_for("{:.{}f}", 1.0, true) );
+  VERIFY( ! is_format_string_for("{:.{}f}", 1.0, "str") );
+  VERIFY( ! is_format_string_for("{:.{}f}", 1.0, nullptr) );
+
   // Invalid presentation types for integers.
   VERIFY( ! is_format_string_for("{:f}", 1) );
   VERIFY( ! is_format_string_for("{:s}", 1) );

Reply via email to