From 79e79a2e0773a949778490f9f59baf1a265fd662 Mon Sep 17 00:00:00 2001 From: "A. Jiang" Date: Sat, 16 Mar 2024 13:09:48 +0800 Subject: [PATCH] Implement `formatter` specializations for `pair` and `tuple` (#4438) Co-authored-by: Stephan T. Lavavej --- docs/cgmanifest.json | 2 +- stl/inc/__msvc_formatter.hpp | 25 +- stl/inc/format | 267 ++++++++++- stl/inc/vector | 4 +- tests/libcxx/expected_results.txt | 14 +- tests/std/test.lst | 1 + .../test.compile.pass.cpp | 3 + .../P2286R8_text_formatting_tuple/env.lst | 4 + .../P2286R8_text_formatting_tuple/test.cpp | 437 ++++++++++++++++++ 9 files changed, 734 insertions(+), 23 deletions(-) create mode 100644 tests/std/tests/P2286R8_text_formatting_tuple/env.lst create mode 100644 tests/std/tests/P2286R8_text_formatting_tuple/test.cpp diff --git a/docs/cgmanifest.json b/docs/cgmanifest.json index 8ec3fb1df..617eae918 100644 --- a/docs/cgmanifest.json +++ b/docs/cgmanifest.json @@ -15,7 +15,7 @@ "type": "git", "git": { "repositoryUrl": "https://github.com/llvm/llvm-project.git", - "commitHash": "b8d38e8b4fcab071c5c4cb698e154023d06de69e" + "commitHash": "2e2b6b53f5f63179b52168ee156df7c76b90bc71" } } }, diff --git a/stl/inc/__msvc_formatter.hpp b/stl/inc/__msvc_formatter.hpp index 5d4232d99..a27138106 100644 --- a/stl/inc/__msvc_formatter.hpp +++ b/stl/inc/__msvc_formatter.hpp @@ -127,9 +127,9 @@ concept _Format_supported_charT = _Is_any_of_v<_CharT, char, wchar_t>; // makes it "disabled" as per N4950 [format.formatter.spec]/5 _EXPORT_STD template struct formatter { - formatter() = delete; - formatter(const formatter&) = delete; - formatter operator=(const formatter&) = delete; + formatter() = delete; + formatter(const formatter&) = delete; + formatter& operator=(const formatter&) = delete; }; _FMT_P2286_BEGIN @@ -266,6 +266,25 @@ struct formatter, _CharT> } #endif // _HAS_CXX23 }; + +#if _HAS_CXX23 +_EXPORT_STD template +struct pair; + +_EXPORT_STD template +class tuple; + +// Specializations for pairs and tuples are forward-declared to avoid any risk of using the disabled primary template. + +// Per LWG-3997, `_CharT` in library-provided `formatter` specializations is +// constrained to character types supported by `format`. + +template <_Format_supported_charT _CharT, class _Ty1, class _Ty2> +struct formatter, _CharT>; + +template <_Format_supported_charT _CharT, class... _Types> +struct formatter, _CharT>; +#endif // _HAS_CXX23 _STD_END #pragma pop_macro("new") diff --git a/stl/inc/format b/stl/inc/format index 4f7a3ffa3..9993ab9f0 100644 --- a/stl/inc/format +++ b/stl/inc/format @@ -58,6 +58,10 @@ _EMIT_STL_WARNING(STL4038, "The contents of are available only with C++ #include #include +#if _HAS_CXX23 +#include +#endif // _HAS_CXX23 + #pragma pack(push, _CRT_PACKING) #pragma warning(push, _STL_WARNING_LEVEL) #pragma warning(disable : _STL_DISABLED_WARNINGS) @@ -3934,6 +3938,259 @@ _NODISCARD size_t formatted_size(const locale& _Loc, const wformat_string<_Types _FMT_P2286_END #if _HAS_CXX23 +enum class _Fmt_tuple_type : uint8_t { _None, _Key_value, _No_brackets }; + +template +struct _Fill_align_and_width_specs { // used by pair, tuple, thread::id, and stacktrace_entry formatters + int _Width = -1; + int _Dynamic_width_index = -1; + _Fmt_align _Alignment = _Fmt_align::_None; + uint8_t _Fill_length = 1; + // At most one codepoint (so one char32_t or four utf-8 char8_t). + _CharT _Fill[4 / sizeof(_CharT)]{' '}; +}; + +template +class _Tuple_format_specs_setter { +public: + constexpr explicit _Tuple_format_specs_setter(_Fill_align_and_width_specs<_CharT>& _Specs_, + _Fmt_tuple_type& _Fmt_type_, basic_format_parse_context<_CharT>& _Parse_ctx_) + : _Specs(_Specs_), _Fmt_type(_Fmt_type_), _Parse_ctx(_Parse_ctx_) {} + + constexpr void _On_align(const _Fmt_align _Aln) { + _Specs._Alignment = _Aln; + } + + constexpr void _On_fill(const basic_string_view<_CharT> _Sv) { + if (_Sv.size() > _STD size(_Specs._Fill)) { + _Throw_format_error("Invalid fill (too long)."); + } + + if (_Sv == _STATICALLY_WIDEN(_CharT, ":")) { + _Throw_format_error(R"(Invalid fill ":" for tuples and pairs.)"); + } + + const auto _Pos = _STD _Copy_unchecked(_Sv._Unchecked_begin(), _Sv._Unchecked_end(), _Specs._Fill); + _STD fill(_Pos, _STD end(_Specs._Fill), _CharT{}); + _Specs._Fill_length = static_cast(_Sv.size()); + } + + constexpr void _On_width(const int _Width) { + _Specs._Width = _Width; + } + + constexpr void _On_dynamic_width(const size_t _Arg_id) { + _Parse_ctx.check_arg_id(_Arg_id); + _Parse_ctx._Check_dynamic_spec_integral(_Arg_id); + _Specs._Dynamic_width_index = _Verify_dynamic_arg_index_in_range(_Arg_id); + } + + constexpr void _On_dynamic_width(_Auto_id_tag) { + const size_t _Arg_id = _Parse_ctx.next_arg_id(); + _Parse_ctx._Check_dynamic_spec_integral(_Arg_id); + _Specs._Dynamic_width_index = _Verify_dynamic_arg_index_in_range(_Arg_id); + } + + constexpr void _On_type(const _CharT _Type) { + if (_Type == _CharT{}) { + return; + } + + if (_Type == static_cast<_CharT>('n')) { + _Fmt_type = _Fmt_tuple_type::_No_brackets; + return; + } + + if constexpr (_IsTwoTuple) { + if (_Type == static_cast<_CharT>('m')) { + _Fmt_type = _Fmt_tuple_type::_Key_value; + return; + } + } + + _Throw_format_error("Invalid type specification."); + } + +private: + _Fill_align_and_width_specs<_CharT>& _Specs; + _Fmt_tuple_type& _Fmt_type; + basic_format_parse_context<_CharT>& _Parse_ctx; + + _NODISCARD static constexpr int _Verify_dynamic_arg_index_in_range(const size_t _Idx) { + if (!_STD in_range(_Idx)) { + _Throw_format_error("Dynamic width index is too large."); + } + + return static_cast(_Idx); + } +}; + +template +_NODISCARD constexpr const _CharT* _Parse_tuple_format_specs( + const _CharT* _Begin, const _CharT* _End, _Callbacks_type&& _Callbacks) { + if (_Begin == _End || *_Begin == '}') { + return _Begin; + } + + _Begin = _STD _Parse_align(_Begin, _End, _Callbacks); + if (_Begin == _End) { + return _Begin; + } + + _Begin = _STD _Parse_width(_Begin, _End, _Callbacks); + if (_Begin == _End) { + return _Begin; + } + + // If there's anything remaining we assume it's a type. + if (*_Begin != '}') { + _Callbacks._On_type(*_Begin); + ++_Begin; + } else { + // call the type callback so it gets a default type, this is required + // since _Specs_checker needs to be able to tell that it got a default type + // to raise an error for default formatted bools with a sign modifier + _Callbacks._On_type(_CharT{}); + } + + return _Begin; +} + +template +constexpr void _Set_tuple_debug_format(_FormatterType& _Formatter, _ParseContext& _Parse_ctx) { + _Formatter.parse(_Parse_ctx); + if constexpr (requires { _Formatter.set_debug_format(); }) { + _Formatter.set_debug_format(); + } +} + +template ... _Types> +class _Tuple_formatter_common_base { +private: + tuple, _CharT>...> _Underlying; + basic_string_view<_CharT> _Separator = _STATICALLY_WIDEN(_CharT, ", "); + basic_string_view<_CharT> _Opening_bracket = _STATICALLY_WIDEN(_CharT, "("); + basic_string_view<_CharT> _Closing_bracket = _STATICALLY_WIDEN(_CharT, ")"); + + _Fill_align_and_width_specs<_CharT> _Specs; + +protected: + static constexpr bool _Is_const_formattable = (formattable && ...); + + template + _FormatContext::iterator _Format(_FormatContext& _Fmt_ctx, _ArgTypes&... _Args) const { + _STL_INTERNAL_STATIC_ASSERT( + (is_same_v<_ArgTypes, remove_reference_t<_Maybe_const<_Is_const_formattable, _Types>>> && ...)); + + auto _Format_specs = _Specs; + if (_Specs._Dynamic_width_index >= 0) { + _Format_specs._Width = + _STD _Get_dynamic_specs<_Width_checker>(_Fmt_ctx.arg(static_cast(_Specs._Dynamic_width_index))); + } + + basic_string<_CharT> _Tmp_buf; + basic_format_context>, _CharT> _Tmp_ctx{ + _STD back_inserter(_Tmp_buf), {}, _Fmt_ctx._Get_lazy_locale()}; + + _STD _Copy_unchecked(_Opening_bracket._Unchecked_begin(), _Opening_bracket._Unchecked_end(), _Tmp_ctx.out()); + [&](index_sequence<_Indices...>) { + auto _Single_writer = [&](auto& _Arg) { + if constexpr (_Idx != 0) { + _STD _Copy_unchecked(_Separator._Unchecked_begin(), _Separator._Unchecked_end(), _Tmp_ctx.out()); + } + _STD get<_Idx>(_Underlying).format(_Arg, _Tmp_ctx); + }; + (_Single_writer.template operator()<_Indices>(_Args), ...); + }(index_sequence_for<_ArgTypes...>{}); + _STD _Copy_unchecked(_Closing_bracket._Unchecked_begin(), _Closing_bracket._Unchecked_end(), _Tmp_ctx.out()); + + return _STD _Write_aligned(_Fmt_ctx.out(), static_cast(_Tmp_buf.size()), _Format_specs, _Fmt_align::_Left, + [&](typename _FormatContext::iterator _Out) { + return _STD _Fmt_write(_STD move(_Out), basic_string_view<_CharT>{_Tmp_buf}); + }); + } + +public: + constexpr void set_separator(const basic_string_view<_CharT> _Sep) noexcept { + _Separator = _Sep; + } + + constexpr void set_brackets( + const basic_string_view<_CharT> _Opening, const basic_string_view<_CharT> _Closing) noexcept { + _Opening_bracket = _Opening; + _Closing_bracket = _Closing; + } + + template + constexpr _ParseContext::iterator parse(_ParseContext& _Ctx) { + _Fmt_tuple_type _Fmt_type = _Fmt_tuple_type::_None; + _Tuple_format_specs_setter<_CharT, sizeof...(_Types) == 2> _Callback{_Specs, _Fmt_type, _Ctx}; + const auto _It = _STD _Parse_tuple_format_specs(_Ctx._Unchecked_begin(), _Ctx._Unchecked_end(), _Callback); + if (_It != _Ctx._Unchecked_end() && *_It != '}') { + _STD _Throw_format_error("Missing '}' in format string."); + } + + if (_Fmt_type == _Fmt_tuple_type::_No_brackets) { + set_brackets({}, {}); + } else if constexpr (sizeof...(_Types) == 2) { + if (_Fmt_type == _Fmt_tuple_type::_Key_value) { + set_separator(_STATICALLY_WIDEN(_CharT, ": ")); + set_brackets({}, {}); + } + } + + _Ctx.advance_to(_Ctx.begin() + (_It - _Ctx._Unchecked_begin())); + _STD apply([&_Ctx](auto&... _Elems) { (_Set_tuple_debug_format(_Elems, _Ctx), ...); }, _Underlying); + + return _Ctx.begin(); + } +}; + +// formatter definition for all pairs and tuples, the deleted default constructor +// makes it "disabled" as per N4971 [format.formatter.spec]/5 + +template +struct _Tuple_formatter_base { + _Tuple_formatter_base() = delete; + _Tuple_formatter_base(const _Tuple_formatter_base&) = delete; + _Tuple_formatter_base& operator=(const _Tuple_formatter_base&) = delete; +}; + +template _Ty1, formattable<_CharT> _Ty2> +struct _Tuple_formatter_base, _CharT> : _Tuple_formatter_common_base<_CharT, _Ty1, _Ty2> { +private: + using _Base = _Tuple_formatter_common_base<_CharT, _Ty1, _Ty2>; + using _Formatted_type = _Maybe_const<_Base::_Is_const_formattable, pair<_Ty1, _Ty2>>; + +public: + template + _FormatContext::iterator format(_Formatted_type& _Elems, _FormatContext& _Ctx) const { + return this->_Format(_Ctx, _Elems.first, _Elems.second); + } +}; + +template ... _Types> +struct _Tuple_formatter_base, _CharT> : _Tuple_formatter_common_base<_CharT, _Types...> { +private: + using _Base = _Tuple_formatter_common_base<_CharT, _Types...>; + using _Formatted_type = _Maybe_const<_Base::_Is_const_formattable, tuple<_Types...>>; + +public: + template + _FormatContext::iterator format(_Formatted_type& _Elems, _FormatContext& _Ctx) const { + return _STD apply([this, &_Ctx](auto&... _Args) { return this->_Format(_Ctx, _Args...); }, _Elems); + } +}; + +// Per LWG-3997, `_CharT` in library-provided `formatter` specializations is +// constrained to character types supported by `format`. + +template <_Format_supported_charT _CharT, class _Ty1, class _Ty2> +struct formatter, _CharT> : _Tuple_formatter_base, _CharT> {}; + +template <_Format_supported_charT _CharT, class... _Types> +struct formatter, _CharT> : _Tuple_formatter_base, _CharT> {}; + enum class _Add_newline : bool { _Nope, _Yes }; _NODISCARD inline string _Unescape_braces(const _Add_newline _Add_nl, const string_view _Old_str) { @@ -3980,16 +4237,6 @@ _NODISCARD inline string _Unescape_braces(const _Add_newline _Add_nl, const stri return _Unescaped_str; } -template -struct _Fill_align_and_width_specs { // used by thread::id and stacktrace_entry formatters - int _Width = -1; - int _Dynamic_width_index = -1; - _Fmt_align _Alignment = _Fmt_align::_None; - uint8_t _Fill_length = 1; - // At most one codepoint (so one char32_t or four utf-8 char8_t). - _CharT _Fill[4 / sizeof(_CharT)] = {' '}; -}; - template class _Fill_align_and_width_specs_setter { public: diff --git a/stl/inc/vector b/stl/inc/vector index 9238b9477..03a273492 100644 --- a/stl/inc/vector +++ b/stl/inc/vector @@ -3578,7 +3578,9 @@ namespace pmr { #endif // _HAS_CXX17 #if _HAS_CXX23 -template +// Per LWG-3997, `_CharT` in library-provided `formatter` specializations is +// constrained to character types supported by `format`. +template requires _Is_specialization_v<_Ty, _Vb_reference> struct formatter<_Ty, _CharT> { private: diff --git a/tests/libcxx/expected_results.txt b/tests/libcxx/expected_results.txt index 0be1a988f..3f43c87b1 100644 --- a/tests/libcxx/expected_results.txt +++ b/tests/libcxx/expected_results.txt @@ -33,6 +33,11 @@ std/utilities/memory/specialized.algorithms/uninitialized.move/uninitialized_mov # LLVM-79783: [libc++][test] array/size_and_alignment.compile.pass.cpp includes non-Standard <__type_traits/datasizeof.h> std/containers/sequences/array/size_and_alignment.compile.pass.cpp FAIL +# LLVM-83734: [libc++][test] Don't include test_format_context.h in parse.pass.cpp +std/containers/sequences/vector.bool/vector.bool.fmt/parse.pass.cpp FAIL +std/thread/thread.threads/thread.thread.class/thread.thread.id/parse.pass.cpp FAIL +std/utilities/format/format.tuple/parse.pass.cpp FAIL + # Non-Standard regex behavior. # "It seems likely that the test is still non-conforming due to how libc++ handles the 'w' character class." std/re/re.traits/lookup_classname.pass.cpp FAIL @@ -259,7 +264,6 @@ std/containers/container.adaptors/container.adaptors.format/types.compile.pass.c std/containers/sequences/vector.bool/vector.bool.fmt/format.functions.format.pass.cpp FAIL std/containers/sequences/vector.bool/vector.bool.fmt/format.functions.vformat.pass.cpp FAIL std/containers/sequences/vector.bool/vector.bool.fmt/format.pass.cpp FAIL -std/containers/sequences/vector.bool/vector.bool.fmt/parse.pass.cpp FAIL std/input.output/iostream.format/print.fun/includes.compile.pass.cpp FAIL std/utilities/format/format.formattable/concept.formattable.compile.pass.cpp FAIL std/utilities/format/format.range/format.range.fmtdef/format.pass.cpp FAIL @@ -285,12 +289,6 @@ std/utilities/format/format.range/format.range.formatter/parse.pass.cpp FAIL std/utilities/format/format.range/format.range.formatter/set_brackets.pass.cpp FAIL std/utilities/format/format.range/format.range.formatter/set_separator.pass.cpp FAIL std/utilities/format/format.range/format.range.formatter/underlying.pass.cpp FAIL -std/utilities/format/format.tuple/format.functions.format.pass.cpp FAIL -std/utilities/format/format.tuple/format.functions.vformat.pass.cpp FAIL -std/utilities/format/format.tuple/format.pass.cpp FAIL -std/utilities/format/format.tuple/parse.pass.cpp FAIL -std/utilities/format/format.tuple/set_brackets.pass.cpp FAIL -std/utilities/format/format.tuple/set_separator.pass.cpp FAIL std/utilities/format/types.compile.pass.cpp FAIL # P2363R5 Extending Associative Containers With The Remaining Heterogeneous Overloads @@ -1112,7 +1110,6 @@ std/input.output/string.streams/stringstream/stringstream.members/str.allocator_ # says "Please create a vendor specific version of the test functions". # If we do, some of these tests should be able to work. std/thread/thread.threads/thread.thread.class/thread.thread.id/format.pass.cpp FAIL -std/thread/thread.threads/thread.thread.class/thread.thread.id/parse.pass.cpp FAIL std/utilities/format/format.formatter/format.context/format.context/advance_to.pass.cpp FAIL std/utilities/format/format.formatter/format.context/format.context/arg.pass.cpp FAIL std/utilities/format/format.formatter/format.context/format.context/ctor.pass.cpp FAIL @@ -1130,6 +1127,7 @@ std/utilities/format/format.formatter/format.formatter.spec/formatter.pointer.pa std/utilities/format/format.formatter/format.formatter.spec/formatter.signed_integral.pass.cpp FAIL std/utilities/format/format.formatter/format.formatter.spec/formatter.string.pass.cpp FAIL std/utilities/format/format.formatter/format.formatter.spec/formatter.unsigned_integral.pass.cpp FAIL +std/utilities/format/format.tuple/format.pass.cpp FAIL # Not analyzed. Apparent false positives from static analysis where it thinks that array indexing is out of bounds. # warning C28020: The expression '0<=_Param_(1)&&_Param_(1)<=1-1' is not true at this call. diff --git a/tests/std/test.lst b/tests/std/test.lst index 5a41397a4..9df15e297 100644 --- a/tests/std/test.lst +++ b/tests/std/test.lst @@ -610,6 +610,7 @@ tests\P2286R8_text_formatting_escaping tests\P2286R8_text_formatting_escaping_legacy_text_encoding tests\P2286R8_text_formatting_escaping_utf8 tests\P2286R8_text_formatting_formattable +tests\P2286R8_text_formatting_tuple tests\P2286R8_text_formatting_vector_bool_reference tests\P2302R4_ranges_alg_contains tests\P2302R4_ranges_alg_contains_subrange diff --git a/tests/std/tests/P2286R8_text_formatting_formattable/test.compile.pass.cpp b/tests/std/tests/P2286R8_text_formatting_formattable/test.compile.pass.cpp index 85b154c52..83897c08b 100644 --- a/tests/std/tests/P2286R8_text_formatting_formattable/test.compile.pass.cpp +++ b/tests/std/tests/P2286R8_text_formatting_formattable/test.compile.pass.cpp @@ -213,6 +213,9 @@ void test_P2286_vector_bool() { // Tests for P2286 Formatting ranges template void test_P2286() { + assert_is_formattable, CharT>(); + assert_is_formattable, CharT>(); + test_P2286_vector_bool>(); test_P2286_vector_bool>(); test_P2286_vector_bool>>(); diff --git a/tests/std/tests/P2286R8_text_formatting_tuple/env.lst b/tests/std/tests/P2286R8_text_formatting_tuple/env.lst new file mode 100644 index 000000000..642f530ff --- /dev/null +++ b/tests/std/tests/P2286R8_text_formatting_tuple/env.lst @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +RUNALL_INCLUDE ..\usual_latest_matrix.lst diff --git a/tests/std/tests/P2286R8_text_formatting_tuple/test.cpp b/tests/std/tests/P2286R8_text_formatting_tuple/test.cpp new file mode 100644 index 000000000..d2b89c1c7 --- /dev/null +++ b/tests/std/tests/P2286R8_text_formatting_tuple/test.cpp @@ -0,0 +1,437 @@ +// Copyright (c) Microsoft Corporation. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// derived from libc++'s test files: +// * std/utilities/format/format.tuple/format.functions.tests.h +// * std/utilities/format/format.tuple/format.functions.format.pass.cpp +// * std/utilities/format/format.tuple/format.functions.vformat.pass.cpp + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; + +#define CSTR(Str) TYPED_LITERAL(CharT, Str) +#define STR(Str) basic_string(CSTR(Str)) +#define SV(Str) basic_string_view(CSTR(Str)) + +enum class color { black, red, gold }; + +template +struct std::formatter : std::formatter, CharT> { + static constexpr basic_string_view color_names[] = {SV("black"), SV("red"), SV("gold")}; + auto format(color c, auto& ctx) const { + return formatter, CharT>::format(color_names[static_cast(c)], ctx); + } +}; + +// +// Generic tests for a tuple and pair with two elements. +// +template +void test_tuple_or_pair_int_int(TestFunction check, ExceptionTest check_exception, TupleOrPair&& input) { + check(SV("(42, 99)"), SV("{}"), input); + check(SV("(42, 99)^42"), SV("{}^42"), input); + check(SV("(42, 99)^42"), SV("{:}^42"), input); + + // *** align-fill & width *** + check(SV("(42, 99) "), SV("{:13}"), input); + check(SV("(42, 99)*****"), SV("{:*<13}"), input); + check(SV("__(42, 99)___"), SV("{:_^13}"), input); + check(SV("#####(42, 99)"), SV("{:#>13}"), input); + + check(SV("(42, 99) "), SV("{:{}}"), input, 13); + check(SV("(42, 99)*****"), SV("{:*<{}}"), input, 13); + check(SV("__(42, 99)___"), SV("{:_^{}}"), input, 13); + check(SV("#####(42, 99)"), SV("{:#>{}}"), input, 13); + + check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input); + check_exception("The fill option contains an invalid value", SV("{:{<}"), input); + check_exception("The fill option contains an invalid value", SV("{::<}"), input); + + // *** sign *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:-}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{:+}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{: }"), input); + + // *** alternate form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:#}"), input); + + // *** zero-padding *** + check_exception("The width option should not have a leading zero", SV("{:0}"), input); + + // *** precision *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:.}"), input); + + // *** locale-specific form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:L}"), input); + + // *** type *** + check(SV("__42: 99___"), SV("{:_^11m}"), input); + check(SV("__42, 99___"), SV("{:_^11n}"), input); + + for (CharT c : SV("aAbBcdeEfFgGopPsxX?")) { + check_exception("The format specifier should consume the input or end with a '}'", + basic_string_view{STR("{:") + c + STR("}")}, input); + } +} + +template +void test_tuple_or_pair_int_string(TestFunction check, ExceptionTest check_exception, TupleOrPair&& input) { + check(SV("(42, \"hello\")"), SV("{}"), input); + check(SV("(42, \"hello\")^42"), SV("{}^42"), input); + check(SV("(42, \"hello\")^42"), SV("{:}^42"), input); + + // *** align-fill & width *** + check(SV("(42, \"hello\") "), SV("{:18}"), input); + check(SV("(42, \"hello\")*****"), SV("{:*<18}"), input); + check(SV("__(42, \"hello\")___"), SV("{:_^18}"), input); + check(SV("#####(42, \"hello\")"), SV("{:#>18}"), input); + + check(SV("(42, \"hello\") "), SV("{:{}}"), input, 18); + check(SV("(42, \"hello\")*****"), SV("{:*<{}}"), input, 18); + check(SV("__(42, \"hello\")___"), SV("{:_^{}}"), input, 18); + check(SV("#####(42, \"hello\")"), SV("{:#>{}}"), input, 18); + + check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input); + check_exception("The fill option contains an invalid value", SV("{:{<}"), input); + check_exception("The fill option contains an invalid value", SV("{::<}"), input); + + // *** sign *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:-}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{:+}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{: }"), input); + + // *** alternate form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:#}"), input); + + // *** zero-padding *** + check_exception("The width option should not have a leading zero", SV("{:0}"), input); + + // *** precision *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:.}"), input); + + // *** locale-specific form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:L}"), input); + + // *** type *** + check(SV("__42: \"hello\"___"), SV("{:_^16m}"), input); + check(SV("__42, \"hello\"___"), SV("{:_^16n}"), input); + + for (CharT c : SV("aAbBcdeEfFgGopPsxX?")) { + check_exception("The format specifier should consume the input or end with a '}'", + basic_string_view{STR("{:") + c + STR("}")}, input); + } +} + +template +void test_escaping(TestFunction check, TupleOrPair&& input) { + static_assert(same_as(input))>, CharT>); + static_assert(same_as(input))>, basic_string>); + + check(SV(R"(('*', ""))"), SV("{}"), input); + + // Char + get<0>(input) = CharT('\t'); + check(SV(R"(('\t', ""))"), SV("{}"), input); + get<0>(input) = CharT('\n'); + check(SV(R"(('\n', ""))"), SV("{}"), input); + get<0>(input) = CharT('\0'); + check(SV(R"(('\u{0}', ""))"), SV("{}"), input); + + // String + get<0>(input) = CharT('*'); + get<1>(input) = SV("hell\u00d6"); + check(SV("('*', \"hell\u00d6\")"), SV("{}"), input); +} + +// +// pair tests +// + +template +void test_pair_int_int(TestFunction check, ExceptionTest check_exception) { + test_tuple_or_pair_int_int(check, check_exception, make_pair(42, 99)); +} + +template +void test_pair_int_string(TestFunction check, ExceptionTest check_exception) { + test_tuple_or_pair_int_string(check, check_exception, make_pair(42, SV("hello"))); + test_tuple_or_pair_int_string(check, check_exception, make_pair(42, STR("hello"))); + test_tuple_or_pair_int_string(check, check_exception, make_pair(42, CSTR("hello"))); +} + +// +// tuple tests +// + +template +void test_tuple_int(TestFunction check, ExceptionTest check_exception) { + auto input = make_tuple(42); + + check(SV("(42)"), SV("{}"), input); + check(SV("(42)^42"), SV("{}^42"), input); + check(SV("(42)^42"), SV("{:}^42"), input); + + // *** align-fill & width *** + check(SV("(42) "), SV("{:9}"), input); + check(SV("(42)*****"), SV("{:*<9}"), input); + check(SV("__(42)___"), SV("{:_^9}"), input); + check(SV("#####(42)"), SV("{:#>9}"), input); + + check(SV("(42) "), SV("{:{}}"), input, 9); + check(SV("(42)*****"), SV("{:*<{}}"), input, 9); + check(SV("__(42)___"), SV("{:_^{}}"), input, 9); + check(SV("#####(42)"), SV("{:#>{}}"), input, 9); + + check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input); + check_exception("The fill option contains an invalid value", SV("{:{<}"), input); + check_exception("The fill option contains an invalid value", SV("{::<}"), input); + + // *** sign *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:-}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{:+}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{: }"), input); + + // *** alternate form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:#}"), input); + + // *** zero-padding *** + check_exception("The width option should not have a leading zero", SV("{:0}"), input); + + // *** precision *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:.}"), input); + + // *** locale-specific form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:L}"), input); + + // *** type *** + check_exception("Type m requires a pair or a tuple with two elements", SV("{:m}"), input); + check(SV("__42___"), SV("{:_^7n}"), input); + + for (CharT c : SV("aAbBcdeEfFgGopPsxX?")) { + check_exception("The format specifier should consume the input or end with a '}'", + basic_string_view{STR("{:") + c + STR("}")}, input); + } +} + +template +void test_tuple_int_string_color(TestFunction check, ExceptionTest check_exception) { + const auto input = make_tuple(42, SV("hello"), color::red); + + check(SV("(42, \"hello\", \"red\")"), SV("{}"), input); + check(SV("(42, \"hello\", \"red\")^42"), SV("{}^42"), input); + check(SV("(42, \"hello\", \"red\")^42"), SV("{:}^42"), input); + + // *** align-fill & width *** + check(SV("(42, \"hello\", \"red\") "), SV("{:25}"), input); + check(SV("(42, \"hello\", \"red\")*****"), SV("{:*<25}"), input); + check(SV("__(42, \"hello\", \"red\")___"), SV("{:_^25}"), input); + check(SV("#####(42, \"hello\", \"red\")"), SV("{:#>25}"), input); + + check(SV("(42, \"hello\", \"red\") "), SV("{:{}}"), input, 25); + check(SV("(42, \"hello\", \"red\")*****"), SV("{:*<{}}"), input, 25); + check(SV("__(42, \"hello\", \"red\")___"), SV("{:_^{}}"), input, 25); + check(SV("#####(42, \"hello\", \"red\")"), SV("{:#>{}}"), input, 25); + + check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input); + check_exception("The fill option contains an invalid value", SV("{:{<}"), input); + check_exception("The fill option contains an invalid value", SV("{::<}"), input); + + // *** sign *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:-}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{:+}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{: }"), input); + + // *** alternate form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:#}"), input); + + // *** zero-padding *** + check_exception("The width option should not have a leading zero", SV("{:0}"), input); + + // *** precision *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:.}"), input); + + // *** locale-specific form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:L}"), input); + + // *** type *** + check_exception("Type m requires a pair or a tuple with two elements", SV("{:m}"), input); + check(SV("__42, \"hello\", \"red\"___"), SV("{:_^23n}"), input); + + for (CharT c : SV("aAbBcdeEfFgGopPsxX?")) { + check_exception("The format specifier should consume the input or end with a '}'", + basic_string_view{STR("{:") + c + STR("}")}, input); + } +} + +template +void test_tuple_int_int(TestFunction check, ExceptionTest check_exception) { + test_tuple_or_pair_int_int(check, check_exception, make_tuple(42, 99)); +} + +template +void test_tuple_int_string(TestFunction check, ExceptionTest check_exception) { + test_tuple_or_pair_int_string(check, check_exception, make_tuple(42, SV("hello"))); + test_tuple_or_pair_int_string(check, check_exception, make_tuple(42, STR("hello"))); + test_tuple_or_pair_int_string(check, check_exception, make_tuple(42, CSTR("hello"))); +} + +template +void test_nested(TestFunction check, ExceptionTest check_exception, Nested&& input) { + // N4971 [format.formatter.spec]/2 + // A debug-enabled specialization of formatter additionally provides a + // public, constexpr, non-static member function set_debug_format() + // which modifies the state of the formatter to be as if the type of the + // std-format-spec parsed by the last call to parse were ?. + // pair and tuple are not debug-enabled specializations so the + // set_debug_format is not propagated. The paper + // P2733 Fix handling of empty specifiers in format + // addressed this. + + check(SV("(42, (\"hello\", \"red\"))"), SV("{}"), input); + check(SV("(42, (\"hello\", \"red\"))^42"), SV("{}^42"), input); + check(SV("(42, (\"hello\", \"red\"))^42"), SV("{:}^42"), input); + + // *** align-fill & width *** + check(SV("(42, (\"hello\", \"red\")) "), SV("{:27}"), input); + check(SV("(42, (\"hello\", \"red\"))*****"), SV("{:*<27}"), input); + check(SV("__(42, (\"hello\", \"red\"))___"), SV("{:_^27}"), input); + check(SV("#####(42, (\"hello\", \"red\"))"), SV("{:#>27}"), input); + + check(SV("(42, (\"hello\", \"red\")) "), SV("{:{}}"), input, 27); + check(SV("(42, (\"hello\", \"red\"))*****"), SV("{:*<{}}"), input, 27); + check(SV("__(42, (\"hello\", \"red\"))___"), SV("{:_^{}}"), input, 27); + check(SV("#####(42, (\"hello\", \"red\"))"), SV("{:#>{}}"), input, 27); + + check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input); + check_exception("The fill option contains an invalid value", SV("{:{<}"), input); + check_exception("The fill option contains an invalid value", SV("{::<}"), input); + + // *** sign *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:-}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{:+}"), input); + check_exception("The format specifier should consume the input or end with a '}'", SV("{: }"), input); + + // *** alternate form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:#}"), input); + + // *** zero-padding *** + check_exception("The width option should not have a leading zero", SV("{:0}"), input); + + // *** precision *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:.}"), input); + + // *** locale-specific form *** + check_exception("The format specifier should consume the input or end with a '}'", SV("{:L}"), input); + + // *** type *** + check(SV("__42: (\"hello\", \"red\")___"), SV("{:_^25m}"), input); + check(SV("__42, (\"hello\", \"red\")___"), SV("{:_^25n}"), input); + + for (CharT c : SV("aAbBcdeEfFgGopPsxX?")) { + check_exception("The format specifier should consume the input or end with a '}'", + basic_string_view{STR("{:") + c + STR("}")}, input); + } +} + +template +void run_tests(TestFunction check, ExceptionTest check_exception) { + test_pair_int_int(check, check_exception); + test_pair_int_string(check, check_exception); + + test_tuple_int(check, check_exception); + test_tuple_int_int(check, check_exception); + test_tuple_int_string(check, check_exception); + test_tuple_int_string_color(check, check_exception); + + test_nested(check, check_exception, make_pair(42, make_pair(SV("hello"), color::red))); + test_nested(check, check_exception, make_pair(42, make_tuple(SV("hello"), color::red))); + test_nested(check, check_exception, make_tuple(42, make_pair(SV("hello"), color::red))); + test_nested(check, check_exception, make_tuple(42, make_tuple(SV("hello"), color::red))); + + test_escaping(check, make_pair(CharT('*'), STR(""))); + test_escaping(check, make_tuple(CharT('*'), STR(""))); + + // Test const ref-qualified types. + // clang-format off + check(SV("(42)"), SV("{}"), tuple< int >{42}); + check(SV("(42)"), SV("{}"), tuple{42}); + + int answer = 42; + check(SV("(42)"), SV("{}"), tuple< int& >{answer}); + check(SV("(42)"), SV("{}"), tuple{answer}); + + check(SV("(42)"), SV("{}"), tuple< int&&>{42}); + check(SV("(42)"), SV("{}"), tuple{42}); + // clang-format on +} + +template +struct format_context_for_impl {}; + +template <> +struct format_context_for_impl { + using type = format_context; +}; + +template <> +struct format_context_for_impl { + using type = wformat_context; +}; + +template +using format_context_for = format_context_for_impl::type; + +auto test_format = [](basic_string_view expected, + type_identity_t> fmt, Args&&... args) { + auto out = format(fmt, forward(args)...); + assert(out == expected); +}; + +auto test_format_exception = [](string_view, basic_string_view, Args&&...) { + // After P2216 most exceptions thrown by format become ill-formed. + // Therefore this test does nothing. + // A basic ill-formed test is done in format.verify.cpp + // The exceptions are tested by other functions that don't use the basic-format-string as fmt argument. +}; + +auto test_vformat = []( + basic_string_view expected, basic_string_view fmt, Args&&... args) { + auto out = vformat(fmt, make_format_args>(args...)); + assert(out == expected); +}; + +auto test_vformat_exception = []([[maybe_unused]] string_view what, + [[maybe_unused]] basic_string_view fmt, [[maybe_unused]] Args&&... args) { + try { + (void) vformat(fmt, make_format_args>(args...)); + assert(false); + } catch (const format_error&) { + } +}; + +int main() { + run_tests(test_format, test_format_exception); + run_tests(test_vformat, test_vformat_exception); + + run_tests(test_format, test_format_exception); + run_tests(test_vformat, test_vformat_exception); +}