Skip to content

Commit ea00050

Browse files
committed
Parse replacement fields of user-defined types at runtime
Replacement fields of user-defined types may now specify formatting options. These will be parsed at runtime just before formatting occurs.
1 parent c94700e commit ea00050

File tree

8 files changed

+221
-51
lines changed

8 files changed

+221
-51
lines changed

build/win/libfly/libfly.vcxproj

+1
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@
240240
<ClInclude Include="..\..\..\fly\types\string\detail\format_specifier.hpp" />
241241
<ClInclude Include="..\..\..\fly\types\string\detail\format_string.hpp" />
242242
<ClInclude Include="..\..\..\fly\types\string\detail\stream_util.hpp" />
243+
<ClInclude Include="..\..\..\fly\types\string\detail\string_concepts.hpp" />
243244
<ClInclude Include="..\..\..\fly\types\string\detail\string_traits.hpp" />
244245
<ClInclude Include="..\..\..\fly\types\string\detail\unicode.hpp" />
245246
<ClInclude Include="..\..\..\fly\types\string\formatters.hpp" />

build/win/libfly/libfly.vcxproj.filters

+3
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,9 @@
325325
<ClInclude Include="..\..\..\fly\types\string\detail\stream_util.hpp">
326326
<Filter>types\string\detail</Filter>
327327
</ClInclude>
328+
<ClInclude Include="..\..\..\fly\types\string\detail\string_concepts.hpp">
329+
<Filter>types\string\detail</Filter>
330+
</ClInclude>
328331
<ClInclude Include="..\..\..\fly\types\string\detail\string_traits.hpp">
329332
<Filter>types\string\detail</Filter>
330333
</ClInclude>

fly/types/string/detail/format_parameters.hpp

+53-9
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
#include "fly/traits/traits.hpp"
44
#include "fly/types/string/detail/classifier.hpp"
5+
#include "fly/types/string/detail/format_parse_context.hpp"
56
#include "fly/types/string/detail/format_specifier.hpp"
7+
#include "fly/types/string/detail/string_concepts.hpp"
68
#include "fly/types/string/detail/string_traits.hpp"
9+
#include "fly/types/string/formatters.hpp"
710

811
#include <array>
912
#include <cstdint>
@@ -27,7 +30,11 @@ struct UserDefinedValue
2730
{
2831
const void *m_value;
2932

30-
void (*m_format)(const void *value, FormatContext &context);
33+
void (*m_format)(
34+
const void *value,
35+
BasicFormatParseContext<typename FormatContext::char_type> &,
36+
FormatContext &context,
37+
BasicFormatSpecifier<typename FormatContext::char_type> &&specifier);
3138
};
3239

3340
/**
@@ -77,10 +84,16 @@ struct StandardValue
7784
* @tparam T The user-defined type.
7885
*
7986
* @param value A pointer to the type-erased user-defined value to format.
87+
* @param parse_context The context holding the format parsing state.
8088
* @param context The context holding the formatting state.
89+
* @param specifier The replacement field to be replaced.
8190
*/
8291
template <typename FormatContext, typename T>
83-
void format_user_defined_value(const void *value, FormatContext &context);
92+
void format_user_defined_value(
93+
const void *value,
94+
BasicFormatParseContext<typename FormatContext::char_type> &parse_context,
95+
FormatContext &context,
96+
BasicFormatSpecifier<typename FormatContext::char_type> &&specifier);
8497

8598
/**
8699
* Re-form a type-erased string value and format that value.
@@ -177,11 +190,14 @@ class BasicFormatParameter
177190
/**
178191
* Apply the type-erased formatting function to the stored format parameter.
179192
*
193+
* @param parse_context The context holding the format parsing state.
180194
* @param context The context holding the formatting state.
181195
* @param specifier The replacement field to be replaced.
182196
*/
183-
constexpr void
184-
format(FormatContext &context, BasicFormatSpecifier<char_type> &&specifier) const;
197+
constexpr void format(
198+
BasicFormatParseContext<typename FormatContext::char_type> &parse_context,
199+
FormatContext &context,
200+
BasicFormatSpecifier<char_type> &&specifier) const;
185201

186202
/**
187203
* Apply the provided visitor to the stored format parameter.
@@ -271,11 +287,34 @@ constexpr inline auto make_format_parameters(ParameterTypes &&...parameters)
271287

272288
//==================================================================================================
273289
template <typename FormatContext, typename T>
274-
inline void format_user_defined_value(const void *value, FormatContext &context)
290+
inline void format_user_defined_value(
291+
const void *value,
292+
BasicFormatParseContext<typename FormatContext::char_type> &parse_context,
293+
FormatContext &context,
294+
BasicFormatSpecifier<typename FormatContext::char_type> &&specifier)
275295
{
276-
typename FormatContext::template formatter_type<T>().format(
277-
*static_cast<const T *>(value),
278-
context);
296+
using Formatter = typename FormatContext::template formatter_type<T>;
297+
298+
Formatter formatter;
299+
parse_context.lexer().set_position(specifier.m_parse_index);
300+
301+
if constexpr (FormatterWithParsing<decltype(parse_context), Formatter>)
302+
{
303+
formatter.parse(parse_context);
304+
}
305+
else
306+
{
307+
if (!parse_context.lexer().consume_if(FLY_CHR(typename FormatContext::char_type, '}')))
308+
{
309+
parse_context.on_error(
310+
"User-defined formatter without a parse method may not have formatting options");
311+
}
312+
}
313+
314+
if (!parse_context.has_error())
315+
{
316+
formatter.format(*static_cast<const T *>(value), context);
317+
}
279318
}
280319

281320
//==================================================================================================
@@ -443,13 +482,18 @@ constexpr inline BasicFormatParameter<FormatContext>::BasicFormatParameter(T val
443482
//==================================================================================================
444483
template <typename FormatContext>
445484
constexpr inline void BasicFormatParameter<FormatContext>::format(
485+
BasicFormatParseContext<typename FormatContext::char_type> &parse_context,
446486
FormatContext &context,
447487
BasicFormatSpecifier<char_type> &&specifier) const
448488
{
449489
switch (m_type)
450490
{
451491
case Type::UserDefined:
452-
m_value.m_user_defined.m_format(m_value.m_user_defined.m_value, context);
492+
m_value.m_user_defined.m_format(
493+
m_value.m_user_defined.m_value,
494+
parse_context,
495+
context,
496+
std::move(specifier));
453497
break;
454498
case Type::String:
455499
m_value.m_string.m_format(

fly/types/string/detail/format_specifier.hpp

-26
Original file line numberDiff line numberDiff line change
@@ -99,32 +99,6 @@ namespace fly::detail {
9999
*
100100
* Boolean types - Valid presentations: none, "c", s", b", "B", "o", "d", "x", "X".
101101
*
102-
* Other user-defined types - Valid presentations: none. To format a user-defined type, a
103-
* fly::Formatter specialization must be defined analagous to std::formatter. The
104-
* specialization may extend a standard fly::Formatter, for example:
105-
*
106-
* template <typename CharType>
107-
* struct fly::Formatter<MyType, CharType> : public fly::Formatter<int, CharType>
108-
* {
109-
* template <typename FormatContext>
110-
* void format(const MyType &value, FormatContext &context)
111-
* {
112-
* fly::Formatter<int, CharType>::format(value.as_int(), context);
113-
* }
114-
* };
115-
*
116-
* Or be defined without inheritence:
117-
*
118-
* template <typename CharType>
119-
* struct fly::Formatter<MyType, CharType>
120-
* {
121-
* template <typename FormatContext>
122-
* void format(const MyType &value, FormatContext &context)
123-
* {
124-
* fly::BasicString<CharType>::format_to(context.out(), "{}", value.as_int());
125-
* }
126-
* };
127-
*
128102
* For details on each presentation type, see the above links.
129103
*
130104
* (*) Nested replacement fields are a subset of the full replacement field, and may be of the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#pragma once
2+
3+
namespace fly::detail {
4+
5+
/**
6+
* Concept that is satisfied when the given formatter defines a |parse| method.
7+
*/
8+
template <typename FormatParseContext, typename Formatter>
9+
concept FormatterWithParsing = requires(FormatParseContext parse_context, Formatter formatter)
10+
{
11+
formatter.parse(parse_context);
12+
};
13+
14+
} // namespace fly::detail

fly/types/string/string.hpp

+56-1
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,55 @@ class BasicString
439439
* compile error with a diagnostic message will be raised. On other compilers, the error message
440440
* will be returned rather than a formatted string.
441441
*
442+
* Replacement fields for user-defined types are parsed at runtime. To format a user-defined
443+
* type, a fly::Formatter specialization must be defined, analagous to std::formatter. The
444+
* specialization may extend a standard fly::Formatter, for example:
445+
*
446+
* template <typename CharType>
447+
* struct fly::Formatter<MyType, CharType> : public fly::Formatter<int, CharType>
448+
* {
449+
* template <typename FormatContext>
450+
* void format(const MyType &value, FormatContext &context)
451+
* {
452+
* fly::Formatter<int, CharType>::format(value.as_int(), context);
453+
* }
454+
* };
455+
*
456+
* Or, the formatter may be defined without without inheritence:
457+
*
458+
* template <typename CharType>
459+
* struct fly::Formatter<MyType, CharType>
460+
* {
461+
* bool m_option {false};
462+
*
463+
* template <typename FormatParseContext>
464+
* constexpr void parse(FormatParseContext &context)
465+
* {
466+
* if (context.lexer().consume_if(FLY_CHR(CharType, 'o')))
467+
* {
468+
* m_option = true;
469+
* }
470+
* if (!context.lexer().consume_if(FLY_CHR(CharType, '}')))
471+
* {
472+
* context.on_error("UserDefinedTypeWithParser error!");
473+
* }
474+
* }
475+
*
476+
* template <typename FormatContext>
477+
* void format(const MyType &value, FormatContext &context)
478+
* {
479+
* fly::BasicString<CharType>::format_to(context.out(), "{}", value.as_int());
480+
* }
481+
* };
482+
*
483+
* The |parse| method is optional. If defined, it is provided a BasicFormatParseContext which
484+
* contains a lexer that may be used to parse the format string. The lexer is positioned such
485+
* that it is pointed at the first character after the ":" in the replacement field (if there is
486+
* one), or after the opening "{" character. The |parse| method is expected to consume up to and
487+
* including the closing "}" character. It may indicate any parsing errors through the parsing
488+
* context; if an error occurs, the error is written to the formatted string, and formatting
489+
* will halt.
490+
*
442491
* @tparam ParameterTypes Variadic format parameter types.
443492
*
444493
* @param fmt The string to format.
@@ -923,7 +972,13 @@ void BasicString<CharType>::format_to(
923972
pos += specifier.m_size;
924973

925974
const auto parameter = context.arg(specifier.m_position);
926-
parameter.format(context, std::move(specifier));
975+
parameter.format(parse_context, context, std::move(specifier));
976+
977+
if (parse_context.has_error())
978+
{
979+
format_to(output, FLY_ARR(char_type, "{}"), parse_context.error());
980+
return;
981+
}
927982
}
928983
break;
929984

test/types/string/format.cpp

+90-14
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ struct UserDefinedType
2424
{
2525
};
2626

27+
struct UserDefinedTypeWithParser
28+
{
29+
};
30+
2731
enum class UserFormattedEnum
2832
{
2933
One = 1,
@@ -76,15 +80,40 @@ StringType reserved_codepoint()
7680
} // namespace
7781

7882
template <typename CharType>
79-
struct fly::Formatter<UserDefinedType, CharType> :
80-
public fly::Formatter<std::basic_string_view<CharType>, CharType>
83+
struct fly::Formatter<UserDefinedType, CharType>
8184
{
8285
template <typename FormatContext>
83-
void format(const UserDefinedType &, FormatContext &context)
86+
void format(UserDefinedType, FormatContext &context)
8487
{
85-
fly::Formatter<std::basic_string_view<CharType>, CharType>::format(
86-
FLY_STR(CharType, "UserDefinedType"),
87-
context);
88+
fly::BasicString<CharType>::format_to(
89+
context.out(),
90+
FLY_ARR(CharType, "{}"),
91+
FLY_STR(CharType, "UserDefinedType"));
92+
}
93+
};
94+
95+
template <typename CharType>
96+
struct fly::Formatter<UserDefinedTypeWithParser, CharType>
97+
{
98+
bool m_option {false};
99+
100+
template <typename FormatParseContext>
101+
constexpr void parse(FormatParseContext &context)
102+
{
103+
if (context.lexer().consume_if(FLY_CHR(CharType, 'o')))
104+
{
105+
m_option = true;
106+
}
107+
if (!context.lexer().consume_if(FLY_CHR(CharType, '}')))
108+
{
109+
context.on_error("UserDefinedTypeWithParser error!");
110+
}
111+
}
112+
113+
template <typename FormatContext>
114+
void format(UserDefinedTypeWithParser, FormatContext &context)
115+
{
116+
fly::BasicString<CharType>::format_to(context.out(), FLY_ARR(CharType, "{}"), m_option);
88117
}
89118
};
90119

@@ -482,14 +511,6 @@ CATCH_TEMPLATE_TEST_CASE("FormatTypes", "[string]", char, wchar_t, char8_t, char
482511
return !value.empty();
483512
};
484513

485-
CATCH_SECTION("User-defined types may be formatted without presentation type")
486-
{
487-
UserDefinedType gt {};
488-
test_format(FMT("{}"), FMT("UserDefinedType"), gt);
489-
test_format(FMT("{}"), FMT("One"), UserFormattedEnum::One);
490-
test_format(FMT("{}"), FMT("Two"), UserFormattedEnum::Two);
491-
}
492-
493514
CATCH_SECTION("Presentation type may be set (character)")
494515
{
495516
test_format(FMT("{:c}"), FMT("a"), 'a');
@@ -748,6 +769,61 @@ CATCH_TEMPLATE_TEST_CASE("FormatTypes", "[string]", char, wchar_t, char8_t, char
748769
}
749770
}
750771

772+
// This test is broken up because otherwise it is too large for Windows and a stack overflow occurs.
773+
CATCH_TEMPLATE_TEST_CASE(
774+
"FormatUserDefinedTypes",
775+
"[string]",
776+
char,
777+
wchar_t,
778+
char8_t,
779+
char16_t,
780+
char32_t)
781+
{
782+
using BasicString = fly::BasicString<TestType>;
783+
784+
using char_type = typename BasicString::char_type;
785+
786+
UserDefinedType u {};
787+
UserDefinedTypeWithParser p {};
788+
789+
CATCH_SECTION("User-defined types inherit parent's parse method")
790+
{
791+
test_format(FMT("{:.1s}"), FMT("O"), UserFormattedEnum::One);
792+
test_format(FMT("{:.2s}"), FMT("On"), UserFormattedEnum::One);
793+
test_format(FMT("{:.3s}"), FMT("One"), UserFormattedEnum::One);
794+
}
795+
796+
CATCH_SECTION("User-defined types may define a parse method")
797+
{
798+
test_format(FMT("{}"), FMT("false"), p);
799+
test_format(FMT("{:o}"), FMT("true"), p);
800+
}
801+
802+
CATCH_SECTION("User-defined types with a parse method may report errors")
803+
{
804+
test_format(FMT("{:x}"), FMT("UserDefinedTypeWithParser error!"), p);
805+
}
806+
807+
CATCH_SECTION("User-defined types do not need to define a parse method")
808+
{
809+
test_format(FMT("{}"), FMT("UserDefinedType"), u);
810+
test_format(FMT("{0}"), FMT("UserDefinedType"), u);
811+
test_format(FMT("{:}"), FMT("UserDefinedType"), u);
812+
}
813+
814+
CATCH_SECTION("User-defined formatter without a parse method may not have formatting options")
815+
{
816+
test_format(
817+
FMT("{:s}"),
818+
FMT("User-defined formatter without a parse method may not have formatting options"),
819+
u);
820+
test_format(
821+
FMT("{:.3}"),
822+
FMT("User-defined formatter without a parse method may not have formatting options"),
823+
u);
824+
}
825+
}
826+
751827
#if defined(FLY_COMPILER_DISABLE_CONSTEVAL)
752828

753829
// This test is broken up because otherwise it is too large for Windows and a stack overflow occurs.

0 commit comments

Comments
 (0)