Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to improve errors in the unformattable case. #3478

Merged
merged 5 commits into from
Jul 1, 2023

Conversation

brevzin
Copy link
Contributor

@brevzin brevzin commented Jun 4, 2023

This is my attempt to improve some some of the compile errors that we get when types aren't formattable at all.

Example 1:

void test(X x, Y y, Z z) {
    fmt::vprint("{} {} {}\n", fmt::make_format_args(x, y, z));
}

Before - you know something is unformattable, but you don't know which one: is it the X, the Y, or the Z?

<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false
ASM generation compiler returned: 1
<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false

After: obviously the Y:

<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1573:63: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 1573 |     type_is_unformattable_for<T, typename Context::char_type> _;
      |                                                               ^
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false
ASM generation compiler returned: 1
<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1573:63: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 1573 |     type_is_unformattable_for<T, typename Context::char_type> _;
      |                                                               ^
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false

Example 2, which is the more typical way people run into this:

void test(X x, Y y, Z z) {
    fmt::print("{} {} {}\n", x, y, z);
}

Before (just copying the first error). Here, you can tell that Y isn't formattable, but the message is about something not being default constructible, which is a little cryptic?

<source>: In instantiation of 'constexpr decltype (ctx.begin()) fmt::v10::detail::parse_format_specs(ParseContext&) [with T = Y; ParseContext = compile_parse_context<char>; decltype (ctx.begin()) = const char*]':
<source>:2607:22:   required from 'constexpr fmt::v10::detail::format_string_checker<Char, Args>::format_string_checker(fmt::v10::basic_string_view<Char>) [with Char = char; Args = {X, Y, Z}]'
<source>:2937:15:   required from here
<source>:2549:10: error: use of deleted function 'fmt::v10::formatter<T, Char, Enable>::formatter() [with T = Y; Char = char; Enable = void]'
 2549 |   return formatter<mapped_type, char_type>().parse(ctx);
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:1066:3: note: declared here
 1066 |   formatter() = delete;
      |   ^~~~~~~~~

After: clearly Y is unformattable:

<source>: In instantiation of 'constexpr decltype (ctx.begin()) fmt::v10::detail::parse_format_specs(ParseContext&) [with T = Y; ParseContext = compile_parse_context<char>; decltype (ctx.begin()) = const char*]':
<source>:2607:22:   required from 'constexpr fmt::v10::detail::format_string_checker<Char, Args>::format_string_checker(fmt::v10::basic_string_view<Char>) [with Char = char; Args = {X, Y, Z}]'
<source>:2937:15:   required from here
<source>:2545:45: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 2545 |     type_is_unformattable_for<T, char_type> _;
      |                                             ^
<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2870:44:   required from 'void fmt::v10::print(format_string<T ...>, T&& ...) [with T = {X&, Y&, Z&}; format_string<T ...> = basic_format_string<char, X&, Y&, Z&>]'
<source>:2937:15:   required from here
<source>:1573:63: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 1573 |     type_is_unformattable_for<T, typename Context::char_type> _;
      |                                                               ^

Maybe it's possible to do better than this, I'm not sure, but this is the best I've been able to come up with so far at least.

@vitaut
Copy link
Contributor

vitaut commented Jun 4, 2023

Having the type name displayed in diagnostic would be nice but unfortunately you can't use if constexpr unconditionally.

@vitaut
Copy link
Contributor

vitaut commented Jun 16, 2023

@brevzin, do you plan to update this PR?

@brevzin
Copy link
Contributor Author

brevzin commented Jun 18, 2023

@brevzin, do you plan to update this PR?

Yeah I will. Are you okay with a solution that just gives the better error in C++17, and maintains status quo before then?

@vitaut
Copy link
Contributor

vitaut commented Jun 18, 2023

Are you okay with a solution that just gives the better error in C++17, and maintains status quo before then?

I am fine with that.

@brevzin
Copy link
Contributor Author

brevzin commented Jun 19, 2023

Are you okay with a solution that just gives the better error in C++17, and maintains status quo before then?

I am fine with that.

Done.

include/fmt/core.h Outdated Show resolved Hide resolved
Comment on lines 1553 to 1557
#if defined(__cpp_if_constexpr)
if constexpr (!formattable_char) {
type_is_unformattable_for<T, typename Context::char_type> _;
}
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message below better explains the error, so let's remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still get both errors - the static_assert message is still printed. This way you also get which character type (and pointer type, for the next one) you were trying to print, which I think is still helpful info.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is this information actually useful? You cannot make the types in question formattable and I think having two errors may be confusing.

Comment on lines 1565 to 1569
#if defined(__cpp_if_constexpr)
if constexpr (!formattable_pointer) {
type_is_unformattable_for<T, typename Context::char_type> _;
}
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Comment on lines +2543 to +2551
#if defined(__cpp_if_constexpr)
if constexpr (std::is_default_constructible_v<
formatter<mapped_type, char_type>>) {
return formatter<mapped_type, char_type>().parse(ctx);
} else {
type_is_unformattable_for<T, char_type> _;
return ctx.begin();
}
#else
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to duplicate the logic here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I could do something like this instead:

if constexpr (/* not default constructible */) {
    type_is_unformattable_for<T, char_type> _;
}
return formatter<mapped_type, char_type>().parse(Ctx);

But doing it this way gives you both the good slightly better error message with type_is_unformattable and also the "not default constructible" error message when you actually try to construct the thing. But all the latter one tells you is that... something failed, whereas the former tells you what failed. I think it's better to just provide the one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why we care about checking default constructibility at all. It should be uncommon and not worth any extra diagnostics.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why we care about checking default constructibility at all. It should be uncommon and not worth any extra diagnostics.

Because that's what actually fails here. Today, if MyType is not formattable and you try to format a MyType, the error that you get is:

<source>:2549:10: error: use of deleted function 'fmt::v10::formatter<T, Char, Enable>::formatter() [with T = MyType; Char = char; Enable = void]'
 2549 |   return formatter<mapped_type, char_type>().parse(ctx);
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

That's the only way of checking if something is formattable, as far as I'm aware. has_formatter does the same thing.

@vitaut vitaut merged commit de4705f into fmtlib:master Jul 1, 2023
40 checks passed
@vitaut
Copy link
Contributor

vitaut commented Jul 1, 2023

Merged, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants