From 41634ec35e8cbad80678c1d1d0043be78e356c1f Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Mon, 29 Dec 2025 13:57:02 -0500 Subject: [PATCH 1/6] Create compilation error tests for FMT_COMPILE missing named argument cases --- test/compile-error-test/CMakeLists.txt | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/compile-error-test/CMakeLists.txt b/test/compile-error-test/CMakeLists.txt index a86996e21f95..09777a62f5d3 100644 --- a/test/compile-error-test/CMakeLists.txt +++ b/test/compile-error-test/CMakeLists.txt @@ -7,6 +7,7 @@ set(fmt_headers " #include #include #include + #include #include ") @@ -209,6 +210,43 @@ if (CMAKE_CXX_STANDARD GREATER_EQUAL 20) #error #endif " ERROR) + + # Format string compilation tests + expect_compile(compiled-format-string-missing-argument-error " + #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS + using namespace fmt::literals; + fmt::format(FMT_COMPILE(\"{x}\")); + #else + #error + #endif + " ERROR) + + expect_compile(compiled-format-string-argument-unassigned-error " + #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS + using namespace fmt::literals; + fmt::format(FMT_COMPILE(\"{x}\"), \"x\"_a); + #else + #error + #endif + " ERROR) + + expect_compile(compiled-format-string-argument-duplicate-error " + #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS + using namespace fmt::literals; + fmt::format(FMT_COMPILE(\"{x}\"), \"x\"_a=42, \"x\"_a=43); + #else + #error + #endif + " ERROR) + + expect_compile(compiled-format-string-missing-other-argument-error " + #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS + using namespace fmt::literals; + fmt::format(FMT_COMPILE(\"{a} {b}\"), \"b\"_a=42); + #else + #error + #endif + " ERROR) endif () # Run all tests From f3ad65c518d6a8a7699713a5375d5e4ebf03cf66 Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Mon, 29 Dec 2025 17:10:13 -0500 Subject: [PATCH 2/6] During format string compilation, static assert that either the format string contains dynamic names, or an argument id is found for every named argument --- include/fmt/compile.h | 53 ++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/include/fmt/compile.h b/include/fmt/compile.h index b21835960439..212d3be7111a 100644 --- a/include/fmt/compile.h +++ b/include/fmt/compile.h @@ -267,13 +267,15 @@ constexpr auto parse_text(basic_string_view str, size_t pos) -> size_t { return pos; } -template +template constexpr auto compile_format_string(S fmt); -template +template constexpr auto parse_tail(T head, S fmt) { if constexpr (POS != basic_string_view(fmt).size()) { - constexpr auto tail = compile_format_string(fmt); + constexpr auto tail = compile_format_string(fmt); if constexpr (std::is_same, unknown_format>()) return tail; @@ -346,14 +348,14 @@ struct field_type::value>> { using type = remove_cvref_t; }; -template +template constexpr auto parse_replacement_field_then_tail(S fmt) { using char_type = typename S::char_type; constexpr auto str = basic_string_view(fmt); constexpr char_type c = END_POS != str.size() ? str[END_POS] : char_type(); if constexpr (c == '}') { - return parse_tail( + return parse_tail( field::type, ARG_INDEX>(), fmt); } else if constexpr (c != ':') { FMT_THROW(format_error("expected ':'")); @@ -364,7 +366,8 @@ constexpr auto parse_replacement_field_then_tail(S fmt) { FMT_THROW(format_error("expected '}'")); return 0; } else { - return parse_tail( + return parse_tail( spec_field::type, ARG_INDEX>{ result.fmt}, fmt); @@ -374,7 +377,7 @@ constexpr auto parse_replacement_field_then_tail(S fmt) { // Compiles a non-empty format string and returns the compiled representation // or unknown_format() on unrecognized input. -template +template constexpr auto compile_format_string(S fmt) { using char_type = typename S::char_type; constexpr auto str = basic_string_view(fmt); @@ -382,14 +385,16 @@ constexpr auto compile_format_string(S fmt) { if constexpr (POS + 1 == str.size()) FMT_THROW(format_error("unmatched '{' in format string")); if constexpr (str[POS + 1] == '{') { - return parse_tail(make_text(str, POS, 1), fmt); + return parse_tail(make_text(str, POS, 1), fmt); } else if constexpr (str[POS + 1] == '}' || str[POS + 1] == ':') { static_assert(ID != manual_indexing_id, "cannot switch from manual to automatic argument indexing"); constexpr auto next_id = ID != manual_indexing_id ? ID + 1 : manual_indexing_id; return parse_replacement_field_then_tail, Args, - POS + 1, ID, next_id>(fmt); + POS + 1, ID, next_id, + DYNAMIC_NAMES>(fmt); } else { constexpr auto arg_id_result = parse_arg_id(str.data() + POS + 1, str.data() + str.size()); @@ -404,19 +409,25 @@ constexpr auto compile_format_string(S fmt) { constexpr auto arg_index = arg_id_result.arg_id.index; return parse_replacement_field_then_tail, Args, arg_id_end_pos, - arg_index, manual_indexing_id>( + arg_index, manual_indexing_id, + DYNAMIC_NAMES>( fmt); } else if constexpr (arg_id_result.kind == arg_id_kind::name) { constexpr auto arg_index = get_arg_index_by_name(arg_id_result.arg_id.name, Args{}); + + static_assert( + arg_index >= 0 || DYNAMIC_NAMES, + "named argument not found"); + if constexpr (arg_index >= 0) { constexpr auto next_id = ID != manual_indexing_id ? ID + 1 : manual_indexing_id; return parse_replacement_field_then_tail< decltype(get_type::value), Args, arg_id_end_pos, - arg_index, next_id>(fmt); + arg_index, next_id, DYNAMIC_NAMES>(fmt); } else if constexpr (c == '}') { - return parse_tail( + return parse_tail( runtime_named_field{arg_id_result.arg_id.name}, fmt); } else if constexpr (c == ':') { return unknown_format(); // no type info for specs parsing @@ -426,13 +437,16 @@ constexpr auto compile_format_string(S fmt) { } else if constexpr (str[POS] == '}') { if constexpr (POS + 1 == str.size()) FMT_THROW(format_error("unmatched '}' in format string")); - return parse_tail(make_text(str, POS, 1), fmt); + return parse_tail(make_text(str, POS, 1), fmt); } else { constexpr auto end = parse_text(str, POS + 1); if constexpr (end - POS > 1) { - return parse_tail(make_text(str, POS, end - POS), fmt); + return parse_tail(make_text(str, POS, end - POS), fmt); } else { - return parse_tail(code_unit{str[POS]}, fmt); + return parse_tail(code_unit{str[POS]}, fmt); } } } @@ -444,8 +458,11 @@ constexpr auto compile(S fmt) { if constexpr (str.size() == 0) { return detail::make_text(str, 0, 0); } else { + constexpr int num_static_named_args = + detail::count_static_named_args(); constexpr auto result = - detail::compile_format_string, 0, 0>(fmt); + detail::compile_format_string, 0, 0, + num_static_named_args != detail::count_named_args()>(fmt); return result; } } @@ -585,4 +602,4 @@ template class static_format_result { FMT_END_EXPORT FMT_END_NAMESPACE -#endif // FMT_COMPILE_H_ +#endif // FMT_COMPILE_H_ \ No newline at end of file From 3c44f2d3003c687901226d66309ea223cf954c56 Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Mon, 29 Dec 2025 20:31:16 -0500 Subject: [PATCH 3/6] Remove duplicate named argument test for now --- test/compile-error-test/CMakeLists.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/compile-error-test/CMakeLists.txt b/test/compile-error-test/CMakeLists.txt index 09777a62f5d3..381bd0d44f14 100644 --- a/test/compile-error-test/CMakeLists.txt +++ b/test/compile-error-test/CMakeLists.txt @@ -230,15 +230,6 @@ if (CMAKE_CXX_STANDARD GREATER_EQUAL 20) #endif " ERROR) - expect_compile(compiled-format-string-argument-duplicate-error " - #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS - using namespace fmt::literals; - fmt::format(FMT_COMPILE(\"{x}\"), \"x\"_a=42, \"x\"_a=43); - #else - #error - #endif - " ERROR) - expect_compile(compiled-format-string-missing-other-argument-error " #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS using namespace fmt::literals; From bc1cb1a2183f4fb89b3192132359b08f801b220d Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Tue, 30 Dec 2025 14:21:49 -0500 Subject: [PATCH 4/6] Fix formatting --- include/fmt/compile.h | 56 ++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/include/fmt/compile.h b/include/fmt/compile.h index 212d3be7111a..9d01ad41f4a5 100644 --- a/include/fmt/compile.h +++ b/include/fmt/compile.h @@ -270,12 +270,12 @@ constexpr auto parse_text(basic_string_view str, size_t pos) -> size_t { template constexpr auto compile_format_string(S fmt); -template +template constexpr auto parse_tail(T head, S fmt) { if constexpr (POS != basic_string_view(fmt).size()) { - constexpr auto tail = compile_format_string(fmt); + constexpr auto tail = + compile_format_string(fmt); if constexpr (std::is_same, unknown_format>()) return tail; @@ -348,8 +348,8 @@ struct field_type::value>> { using type = remove_cvref_t; }; -template +template constexpr auto parse_replacement_field_then_tail(S fmt) { using char_type = typename S::char_type; constexpr auto str = basic_string_view(fmt); @@ -366,8 +366,8 @@ constexpr auto parse_replacement_field_then_tail(S fmt) { FMT_THROW(format_error("expected '}'")); return 0; } else { - return parse_tail( + return parse_tail( spec_field::type, ARG_INDEX>{ result.fmt}, fmt); @@ -385,16 +385,15 @@ constexpr auto compile_format_string(S fmt) { if constexpr (POS + 1 == str.size()) FMT_THROW(format_error("unmatched '{' in format string")); if constexpr (str[POS + 1] == '{') { - return parse_tail(make_text(str, POS, 1), fmt); + return parse_tail( + make_text(str, POS, 1), fmt); } else if constexpr (str[POS + 1] == '}' || str[POS + 1] == ':') { static_assert(ID != manual_indexing_id, "cannot switch from manual to automatic argument indexing"); constexpr auto next_id = ID != manual_indexing_id ? ID + 1 : manual_indexing_id; - return parse_replacement_field_then_tail, Args, - POS + 1, ID, next_id, - DYNAMIC_NAMES>(fmt); + return parse_replacement_field_then_tail< + get_type, Args, POS + 1, ID, next_id, DYNAMIC_NAMES>(fmt); } else { constexpr auto arg_id_result = parse_arg_id(str.data() + POS + 1, str.data() + str.size()); @@ -407,18 +406,15 @@ constexpr auto compile_format_string(S fmt) { ID == manual_indexing_id || ID == 0, "cannot switch from automatic to manual argument indexing"); constexpr auto arg_index = arg_id_result.arg_id.index; - return parse_replacement_field_then_tail, - Args, arg_id_end_pos, - arg_index, manual_indexing_id, - DYNAMIC_NAMES>( - fmt); + return parse_replacement_field_then_tail< + get_type, Args, arg_id_end_pos, arg_index, + manual_indexing_id, DYNAMIC_NAMES>(fmt); } else if constexpr (arg_id_result.kind == arg_id_kind::name) { constexpr auto arg_index = get_arg_index_by_name(arg_id_result.arg_id.name, Args{}); - static_assert( - arg_index >= 0 || DYNAMIC_NAMES, - "named argument not found"); + static_assert(arg_index >= 0 || DYNAMIC_NAMES, + "named argument not found"); if constexpr (arg_index >= 0) { constexpr auto next_id = @@ -437,16 +433,16 @@ constexpr auto compile_format_string(S fmt) { } else if constexpr (str[POS] == '}') { if constexpr (POS + 1 == str.size()) FMT_THROW(format_error("unmatched '}' in format string")); - return parse_tail(make_text(str, POS, 1), fmt); + return parse_tail(make_text(str, POS, 1), + fmt); } else { constexpr auto end = parse_text(str, POS + 1); if constexpr (end - POS > 1) { - return parse_tail(make_text(str, POS, end - POS), fmt); + return parse_tail( + make_text(str, POS, end - POS), fmt); } else { - return parse_tail(code_unit{str[POS]}, fmt); + return parse_tail( + code_unit{str[POS]}, fmt); } } } @@ -460,9 +456,9 @@ constexpr auto compile(S fmt) { } else { constexpr int num_static_named_args = detail::count_static_named_args(); - constexpr auto result = - detail::compile_format_string, 0, 0, - num_static_named_args != detail::count_named_args()>(fmt); + constexpr auto result = detail::compile_format_string< + detail::type_list, 0, 0, + num_static_named_args != detail::count_named_args()>(fmt); return result; } } From d22d2d5dc7ed6403d49a1576d6981ca71de3ef25 Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Fri, 2 Jan 2026 21:56:47 -0500 Subject: [PATCH 5/6] Add missing newline --- include/fmt/compile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/fmt/compile.h b/include/fmt/compile.h index 9d01ad41f4a5..5c6820f7044c 100644 --- a/include/fmt/compile.h +++ b/include/fmt/compile.h @@ -598,4 +598,4 @@ template class static_format_result { FMT_END_EXPORT FMT_END_NAMESPACE -#endif // FMT_COMPILE_H_ \ No newline at end of file +#endif // FMT_COMPILE_H_ From 8dd4b481207a6daf727538b9518ee114c0a67101 Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Fri, 2 Jan 2026 21:58:06 -0500 Subject: [PATCH 6/6] Remove compilation error tests --- test/compile-error-test/CMakeLists.txt | 28 -------------------------- 1 file changed, 28 deletions(-) diff --git a/test/compile-error-test/CMakeLists.txt b/test/compile-error-test/CMakeLists.txt index 381bd0d44f14..9b2de8cba12f 100644 --- a/test/compile-error-test/CMakeLists.txt +++ b/test/compile-error-test/CMakeLists.txt @@ -210,34 +210,6 @@ if (CMAKE_CXX_STANDARD GREATER_EQUAL 20) #error #endif " ERROR) - - # Format string compilation tests - expect_compile(compiled-format-string-missing-argument-error " - #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS - using namespace fmt::literals; - fmt::format(FMT_COMPILE(\"{x}\")); - #else - #error - #endif - " ERROR) - - expect_compile(compiled-format-string-argument-unassigned-error " - #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS - using namespace fmt::literals; - fmt::format(FMT_COMPILE(\"{x}\"), \"x\"_a); - #else - #error - #endif - " ERROR) - - expect_compile(compiled-format-string-missing-other-argument-error " - #if defined(FMT_HAS_CONSTEVAL) && FMT_USE_NONTYPE_TEMPLATE_ARGS - using namespace fmt::literals; - fmt::format(FMT_COMPILE(\"{a} {b}\"), \"b\"_a=42); - #else - #error - #endif - " ERROR) endif () # Run all tests