From 83befec360e36fdbe37b8585ca6d89d5f8ebe4cc Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 3 Feb 2026 13:00:34 +0100 Subject: [PATCH 1/6] Use fmt::runtime Otherwise fmt tries to do compile-time checks, but fails as it cannot be checked at compile time. https://github.com/fmtlib/fmt/issues/4179 --- include/bout/boutexception.hxx | 15 ++++--- include/bout/msg_stack.hxx | 6 +-- include/bout/optionsreader.hxx | 4 +- include/bout/output.hxx | 14 ++++--- include/bout/sys/expressionparser.hxx | 2 +- src/bout++.cxx | 2 +- src/mesh/impls/bout/boutmesh.cxx | 59 +++++++++++++++------------ src/sys/expressionparser.cxx | 10 +++-- src/sys/options.cxx | 15 ++++--- src/sys/options/options_ini.cxx | 2 +- tests/unit/sys/test_options.cxx | 22 +++++----- 11 files changed, 86 insertions(+), 65 deletions(-) diff --git a/include/bout/boutexception.hxx b/include/bout/boutexception.hxx index 238c88654c..6445505831 100644 --- a/include/bout/boutexception.hxx +++ b/include/bout/boutexception.hxx @@ -19,8 +19,9 @@ public: BoutException(std::string msg); template - BoutException(const S& format, const Args&... args) - : BoutException(fmt::format(format, args...)) {} + BoutException(S&& format, Args&&... args) + : BoutException(fmt::format(fmt::runtime(std::forward(format)), + std::forward(args)...)) {} ~BoutException() override; @@ -46,16 +47,18 @@ class BoutRhsFail : public BoutException { public: BoutRhsFail(std::string message) : BoutException(std::move(message)) {} template - BoutRhsFail(const S& format, const Args&... args) - : BoutRhsFail(fmt::format(format, args...)) {} + BoutRhsFail(S&& format, Args&&... args) + : BoutRhsFail(fmt::format(fmt::runtime(std::forward(format)), + std::forward(args)...)) {} }; class BoutIterationFail : public BoutException { public: BoutIterationFail(std::string message) : BoutException(std::move(message)) {} template - BoutIterationFail(const S& format, const Args&... args) - : BoutIterationFail(fmt::format(format, args...)) {} + BoutIterationFail(S&& format, Args&&... args) + : BoutIterationFail(fmt::format(fmt::runtime(std::forward(format)), + std::forward(args)...)) {} }; #endif diff --git a/include/bout/msg_stack.hxx b/include/bout/msg_stack.hxx index 6d19469e25..961d500aa0 100644 --- a/include/bout/msg_stack.hxx +++ b/include/bout/msg_stack.hxx @@ -76,7 +76,7 @@ public: template int push(const S& format, const Args&... args) { - return push(fmt::format(format, args...)); + return push(fmt::format(fmt::runtime(format), args...)); } void pop(); ///< Remove the last message @@ -145,8 +145,8 @@ public: template MsgStackItem(const std::string& file, int line, const S& msg, const Args&... args) - : point(msg_stack.push("{:s} on line {:d} of '{:s}'", fmt::format(msg, args...), - line, file)) {} + : point(msg_stack.push("{:s} on line {:d} of '{:s}'", + fmt::format(fmt::runtime(msg), args...), line, file)) {} ~MsgStackItem() { // If an exception has occurred, don't pop the message if (exception_count == std::uncaught_exceptions()) { diff --git a/include/bout/optionsreader.hxx b/include/bout/optionsreader.hxx index de3d40514d..0c9c227916 100644 --- a/include/bout/optionsreader.hxx +++ b/include/bout/optionsreader.hxx @@ -71,7 +71,7 @@ public: template void read(Options* options, const S& format, const Args&... args) { - return read(options, fmt::format(format, args...)); + return read(options, fmt::format(fmt::runtime(format), args...)); } /// Write options to file @@ -82,7 +82,7 @@ public: template void write(Options* options, const S& format, const Args&... args) { - return write(options, fmt::format(format, args...)); + return write(options, fmt::format(fmt::runtime(format), args...)); } /// Parse options from the command line diff --git a/include/bout/output.hxx b/include/bout/output.hxx index 2862899067..aa8264cf53 100644 --- a/include/bout/output.hxx +++ b/include/bout/output.hxx @@ -76,7 +76,7 @@ public: } template - Output(const S& format, const Args&... args) : Output(fmt::format(format, args...)) {} + Output(const S& format, const Args&... args) : Output(fmt::format(fmt::runtime(format), args...)) {} ~Output() override { close(); } @@ -88,7 +88,7 @@ public: template int open(const S& format, const Args&... args) { - return open(fmt::format(format, args...)); + return open(fmt::format(fmt::runtime(format), args...)); } /// Close the log file @@ -99,14 +99,14 @@ public: template void write(const S& format, const Args&... args) { - write(fmt::format(format, args...)); + write(fmt::format(fmt::runtime(format), args...)); } /// Same as write, but only to screen virtual void print(const std::string& message); template void print(const S& format, const Args&... args) { - print(fmt::format(format, args...)); + print(fmt::format(fmt::runtime(format), args...)); } /// Add an output stream. All output will be sent to all streams @@ -173,7 +173,8 @@ public: void write(const S& format, const Args&... args) { if (enabled) { ASSERT1(base != nullptr); - base->write(fmt::format(format, args...)); + base->write( + fmt::format(fmt::runtime(format), std::forward(args)...)); } } @@ -185,7 +186,8 @@ public: void print(const S& format, const Args&... args) { if (enabled) { ASSERT1(base != nullptr); - base->print(fmt::format(format, args...)); + base->print( + fmt::format(fmt::runtime(format), std::forward(args)...)); } } diff --git a/include/bout/sys/expressionparser.hxx b/include/bout/sys/expressionparser.hxx index 660ad20ab3..4b1a8a1bcc 100644 --- a/include/bout/sys/expressionparser.hxx +++ b/include/bout/sys/expressionparser.hxx @@ -249,7 +249,7 @@ public: template ParseException(const S& format, const Args&... args) - : message(fmt::format(format, args...)) {} + : message(fmt::format(fmt::runtime(format), args...)) {} ~ParseException() override = default; diff --git a/src/bout++.cxx b/src/bout++.cxx index 6de1c5713b..a1f3ec0477 100644 --- a/src/bout++.cxx +++ b/src/bout++.cxx @@ -337,7 +337,7 @@ template // type. Note that this does require all the options are used in the // constructor, and not in a `init` method or similar std::cout << fmt::format("Input options for {} '{}':\n\n", Factory::type_name, type); - std::cout << fmt::format("{:id}\n", help_options); + std::cout << fmt::format(fmt::runtime("{:id}\n"), help_options); std::exit(EXIT_SUCCESS); } diff --git a/src/mesh/impls/bout/boutmesh.cxx b/src/mesh/impls/bout/boutmesh.cxx index 574902ea7b..d48ae9e6f0 100644 --- a/src/mesh/impls/bout/boutmesh.cxx +++ b/src/mesh/impls/bout/boutmesh.cxx @@ -163,14 +163,16 @@ CheckMeshResult checkBoutMeshYDecomposition(int num_y_processors, int ny, // Check size of Y mesh if we've got multiple processors in Y if (num_local_y_points < num_y_guards and num_y_processors != 1) { return {false, - fmt::format(_("\t -> ny/NYPE ({:d}/{:d} = {:d}) must be >= MYG ({:d})\n"), ny, - num_y_processors, num_local_y_points, num_y_guards)}; + fmt::format(fmt::runtime(_( + "\t -> ny/NYPE ({:d}/{:d} = {:d}) must be >= MYG ({:d})\n")), + ny, num_y_processors, num_local_y_points, num_y_guards)}; } // Check branch cuts if ((jyseps1_1 + 1) % num_local_y_points != 0) { - return {false, fmt::format(_("\t -> Leg region jyseps1_1+1 ({:d}) must be a " - "multiple of MYSUB ({:d})\n"), - jyseps1_1 + 1, num_local_y_points)}; + return {false, + fmt::format(fmt::runtime(_("\t -> Leg region jyseps1_1+1 ({:d}) must be a " + "multiple of MYSUB ({:d})\n")), + jyseps1_1 + 1, num_local_y_points)}; } if (jyseps2_1 != jyseps1_2) { @@ -179,50 +181,57 @@ CheckMeshResult checkBoutMeshYDecomposition(int num_y_processors, int ny, if ((jyseps2_1 - jyseps1_1) % num_local_y_points != 0) { return { false, - fmt::format(_("\t -> Core region jyseps2_1-jyseps1_1 ({:d}-{:d} = {:d}) must " - "be a multiple of MYSUB ({:d})\n"), + fmt::format(fmt::runtime(_( + "\t -> Core region jyseps2_1-jyseps1_1 ({:d}-{:d} = {:d}) must " + "be a multiple of MYSUB ({:d})\n")), jyseps2_1, jyseps1_1, jyseps2_1 - jyseps1_1, num_local_y_points)}; } if ((jyseps2_2 - jyseps1_2) % num_local_y_points != 0) { return { false, - fmt::format(_("\t -> Core region jyseps2_2-jyseps1_2 ({:d}-{:d} = {:d}) must " - "be a multiple of MYSUB ({:d})\n"), + fmt::format(fmt::runtime(_( + "\t -> Core region jyseps2_2-jyseps1_2 ({:d}-{:d} = {:d}) must " + "be a multiple of MYSUB ({:d})\n")), jyseps2_2, jyseps1_2, jyseps2_2 - jyseps1_2, num_local_y_points)}; } // Check upper legs if ((ny_inner - jyseps2_1 - 1) % num_local_y_points != 0) { - return { - false, - fmt::format(_("\t -> leg region ny_inner-jyseps2_1-1 ({:d}-{:d}-1 = {:d}) must " - "be a multiple of MYSUB ({:d})\n"), - ny_inner, jyseps2_1, ny_inner - jyseps2_1 - 1, num_local_y_points)}; + return {false, + fmt::format( + fmt::runtime( + _("\t -> leg region ny_inner-jyseps2_1-1 ({:d}-{:d}-1 = {:d}) must " + "be a multiple of MYSUB ({:d})\n")), + ny_inner, jyseps2_1, ny_inner - jyseps2_1 - 1, num_local_y_points)}; } if ((jyseps1_2 - ny_inner + 1) % num_local_y_points != 0) { - return { - false, - fmt::format(_("\t -> leg region jyseps1_2-ny_inner+1 ({:d}-{:d}+1 = {:d}) must " - "be a multiple of MYSUB ({:d})\n"), - jyseps1_2, ny_inner, jyseps1_2 - ny_inner + 1, num_local_y_points)}; + return {false, + fmt::format( + fmt::runtime( + _("\t -> leg region jyseps1_2-ny_inner+1 ({:d}-{:d}+1 = {:d}) must " + "be a multiple of MYSUB ({:d})\n")), + jyseps1_2, ny_inner, jyseps1_2 - ny_inner + 1, num_local_y_points)}; } } else { // Single Null if ((jyseps2_2 - jyseps1_1) % num_local_y_points != 0) { return { false, - fmt::format(_("\t -> Core region jyseps2_2-jyseps1_1 ({:d}-{:d} = {:d}) must " - "be a multiple of MYSUB ({:d})\n"), + fmt::format(fmt::runtime(_( + "\t -> Core region jyseps2_2-jyseps1_1 ({:d}-{:d} = {:d}) must " + "be a multiple of MYSUB ({:d})\n")), jyseps2_2, jyseps1_1, jyseps2_2 - jyseps1_1, num_local_y_points)}; } } if ((ny - jyseps2_2 - 1) % num_local_y_points != 0) { - return {false, fmt::format( - _("\t -> leg region ny-jyseps2_2-1 ({:d}-{:d}-1 = {:d}) must be a " - "multiple of MYSUB ({:d})\n"), - ny, jyseps2_2, ny - jyseps2_2 - 1, num_local_y_points)}; + return { + false, + fmt::format(fmt::runtime(_( + "\t -> leg region ny-jyseps2_2-1 ({:d}-{:d}-1 = {:d}) must be a " + "multiple of MYSUB ({:d})\n")), + ny, jyseps2_2, ny - jyseps2_2 - 1, num_local_y_points)}; } return {true, ""}; diff --git a/src/sys/expressionparser.cxx b/src/sys/expressionparser.cxx index 8290a4cae0..806e4c4804 100644 --- a/src/sys/expressionparser.cxx +++ b/src/sys/expressionparser.cxx @@ -333,13 +333,15 @@ may need to change your input file. // No matches, just point out the error if (possible_matches.empty()) { - return fmt::format(message_template, name, problem_bit); + return fmt::format(fmt::runtime(message_template), name, problem_bit); } // Give the first suggestion as a possible alternative - std::string error_message = fmt::format(message_template, name, problem_bit); - error_message += fmt::format(_("\n {1: ^{2}}{0}\n Did you mean '{0}'?"), - possible_matches.begin()->name, "", start); + std::string error_message = + fmt::format(fmt::runtime(message_template), name, problem_bit); + error_message += + fmt::format(fmt::runtime(_("\n {1: ^{2}}{0}\n Did you mean '{0}'?")), + possible_matches.begin()->name, "", start); return error_message; }; diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 24c9e933c8..132daf8e83 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -747,8 +747,9 @@ Array Options::as>(const Array& similar_to) Array result = bout::utils::visit( ConvertContainer>{ - fmt::format( - _("Value for option {:s} cannot be converted to an Array"), + fmt::format(fmt::runtime( + _("Value for option {:s} cannot be converted to an Array") + ), full_name), similar_to}, value); @@ -1083,20 +1084,22 @@ bout::details::OptionsFormatterBase::format(const Options& options, // Get all the child values first for (const auto& child : children) { if (child.second.isValue()) { - fmt::format_to(ctx.out(), format_string, child.second); + fmt::format_to(ctx.out(), fmt::runtime(format_string), child.second); fmt::format_to(ctx.out(), "\n"); } } // Now descend the tree, accumulating subsections for (const auto& subsection : options.subsections()) { - fmt::format_to(ctx.out(), format_string, *subsection.second); + fmt::format_to(ctx.out(), fmt::runtime(format_string), *subsection.second); } return ctx.out(); } -std::string toString(const Options& value) { return fmt::format("{}", value); } +std::string toString(const Options& value) { + return fmt::format(fmt::runtime("{}"), value); +} namespace bout { void checkForUnusedOptions() { @@ -1142,7 +1145,7 @@ void checkForUnusedOptions(const Options& options, const std::string& data_dir, } possible_misspellings += fmt::format("\nUnused option '{}', did you mean:\n", key); for (const auto& match : fuzzy_matches) { - possible_misspellings += fmt::format("\t{:idk}\n", match.match); + possible_misspellings += fmt::format(fmt::runtime("\t{:idk}\n"), match.match); } } diff --git a/src/sys/options/options_ini.cxx b/src/sys/options/options_ini.cxx index d1889f993b..3e1b1f8b52 100644 --- a/src/sys/options/options_ini.cxx +++ b/src/sys/options/options_ini.cxx @@ -161,7 +161,7 @@ void OptionINI::write(Options* options, const std::string& filename) { } // Call recursive function to write to file - fout << fmt::format("{:uds}", *options); + fout << fmt::format(fmt::runtime("{:uds}"), *options); fout.close(); } diff --git a/tests/unit/sys/test_options.cxx b/tests/unit/sys/test_options.cxx index a9a3bf4af4..4035eed95c 100644 --- a/tests/unit/sys/test_options.cxx +++ b/tests/unit/sys/test_options.cxx @@ -1099,7 +1099,9 @@ value6 = 12 } TEST_F(OptionsTest, InvalidFormat) { - EXPECT_THROW([[maybe_unused]] auto none = fmt::format("{:nope}", Options{}), fmt::format_error); + EXPECT_THROW([[maybe_unused]] auto none = + fmt::format(fmt::runtime("{:nope}"), Options{}), + fmt::format_error); } TEST_F(OptionsTest, FormatValue) { @@ -1110,7 +1112,7 @@ TEST_F(OptionsTest, FormatValue) { const std::string expected = "value1 = 4 # type: int, doc: This is a value, source: some test"; - EXPECT_EQ(expected, fmt::format("{:ds}", options["value1"])); + EXPECT_EQ(expected, fmt::format(fmt::runtime("{:ds}"), options["value1"])); } TEST_F(OptionsTest, FormatDefault) { @@ -1138,7 +1140,7 @@ value4 = 3.2 value6 = 12 )"; - EXPECT_EQ(fmt::format("{}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{}"), option), expected); } TEST_F(OptionsTest, FormatDocstrings) { @@ -1169,7 +1171,7 @@ value4 = 3.2 value6 = 12 )"; - EXPECT_EQ(fmt::format("{:d}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:d}"), option), expected); } TEST_F(OptionsTest, FormatDocstringsAndInline) { @@ -1192,9 +1194,9 @@ section2:subsection1:value4 = 3.2 section3:subsection2:value6 = 12 )"; - EXPECT_EQ(fmt::format("{:di}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:di}"), option), expected); // Order of format spec shouldn't matter - EXPECT_EQ(fmt::format("{:id}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:id}"), option), expected); } TEST_F(OptionsTest, FormatDocstringsAndInlineKeysOnly) { @@ -1219,16 +1221,16 @@ section2:subsection1:value4 section3:subsection2:value6 # source: a test )"; - EXPECT_EQ(fmt::format("{:ksdi}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:ksdi}"), option), expected); // Order of format spec shouldn't matter - EXPECT_EQ(fmt::format("{:idsk}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:idsk}"), option), expected); } TEST_F(OptionsTest, FormatUnused) { Options option{{"section1", {{"value1", 42}}}}; std::string expected = "section1:value1\t\t# unused value (NOT marked conditionally used)\n"; - EXPECT_EQ(fmt::format("{:iku}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:iku}"), option), expected); } TEST_F(OptionsTest, FormatConditionallyUsed) { @@ -1236,7 +1238,7 @@ TEST_F(OptionsTest, FormatConditionallyUsed) { option.setConditionallyUsed(); std::string expected = "section1:value1\t\t# unused value (marked conditionally used)\n"; - EXPECT_EQ(fmt::format("{:iku}", option), expected); + EXPECT_EQ(fmt::format(fmt::runtime("{:iku}"), option), expected); } TEST_F(OptionsTest, GetUnused) { From 510d2e6fe1554672afbb794f32de82fe07b14cac Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 3 Feb 2026 13:00:47 +0100 Subject: [PATCH 2/6] Expose erase even with C++20 --- include/bout/utils.hxx | 1 + 1 file changed, 1 insertion(+) diff --git a/include/bout/utils.hxx b/include/bout/utils.hxx index e2ac814e53..9bd2301dae 100644 --- a/include/bout/utils.hxx +++ b/include/bout/utils.hxx @@ -187,6 +187,7 @@ typename std::vector::size_type erase_if(std::vector& c, Pre return r; } #else +using std::erase; using std::erase_if; #endif } // namespace utils From 0932af4a51ef53eaeed83ada3063445c01b05475 Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 3 Feb 2026 13:01:47 +0100 Subject: [PATCH 3/6] Do not pass by reference With C++ std::accumulate uses std::move, which fails with the reference. --- tests/unit/fake_mesh.hxx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/unit/fake_mesh.hxx b/tests/unit/fake_mesh.hxx index e6f78f8767..0123eae3a2 100644 --- a/tests/unit/fake_mesh.hxx +++ b/tests/unit/fake_mesh.hxx @@ -212,18 +212,17 @@ public: "RGN_OUTER_X"}; // Sum up and get unique points in the boundaries defined above - addRegion2D("RGN_BNDRY", - std::accumulate(begin(boundary_names), end(boundary_names), - Region{}, - [this](Region& a, const std::string& b) { - return a + getRegion2D(b); - }) - .unique()); + addRegion2D("RGN_BNDRY", std::accumulate(begin(boundary_names), end(boundary_names), + Region{}, + [&](Region a, const std::string& b) { + return a + getRegion2D(b); + }) + .unique()); addRegion3D("RGN_BNDRY", std::accumulate(begin(boundary_names), end(boundary_names), Region{}, - [this](Region& a, const std::string& b) { + [this](Region a, const std::string& b) { return a + getRegion3D(b); }) .unique()); From 683f6feb0cdf1e90d142e4ef5021d99b3e0ddd7d Mon Sep 17 00:00:00 2001 From: David Bold Date: Tue, 3 Feb 2026 13:03:14 +0100 Subject: [PATCH 4/6] Do not include unneeded header isatty is provided by both cpptrace and unistd.h, so we should only include one. --- src/sys/boutexception.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sys/boutexception.cxx b/src/sys/boutexception.cxx index 825fb37e28..4b5ade1bb3 100644 --- a/src/sys/boutexception.cxx +++ b/src/sys/boutexception.cxx @@ -3,7 +3,6 @@ #include #include #include -#include #include #if BOUT_USE_BACKTRACE From 4b9d679567620aa77f847993f22b23fbbe972dd0 Mon Sep 17 00:00:00 2001 From: David Bold Date: Wed, 4 Feb 2026 12:44:18 +0100 Subject: [PATCH 5/6] Switch remaining functions to fmt::runtime --- src/sys/options.cxx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 132daf8e83..ddbaf8e793 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -771,7 +771,8 @@ Matrix Options::as>(const Matrix& similar_t auto result = bout::utils::visit( ConvertContainer>{ fmt::format( - _("Value for option {:s} cannot be converted to an Matrix"), + fmt::runtime( + _("Value for option {:s} cannot be converted to an Matrix")), full_name), similar_to}, value); @@ -793,7 +794,8 @@ Tensor Options::as>(const Tensor& similar_t auto result = bout::utils::visit( ConvertContainer>{ fmt::format( - _("Value for option {:s} cannot be converted to an Tensor"), + fmt::runtime( + _("Value for option {:s} cannot be converted to an Tensor")), full_name), similar_to}, value); From ecb03f320784534ec186fc1966aa00fa6c051577 Mon Sep 17 00:00:00 2001 From: dschwoerer <5637662+dschwoerer@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:47:47 +0000 Subject: [PATCH 6/6] Apply clang-format changes --- include/bout/output.hxx | 3 ++- src/sys/options.cxx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/bout/output.hxx b/include/bout/output.hxx index aa8264cf53..978ca0d7d8 100644 --- a/include/bout/output.hxx +++ b/include/bout/output.hxx @@ -76,7 +76,8 @@ public: } template - Output(const S& format, const Args&... args) : Output(fmt::format(fmt::runtime(format), args...)) {} + Output(const S& format, const Args&... args) + : Output(fmt::format(fmt::runtime(format), args...)) {} ~Output() override { close(); } diff --git a/src/sys/options.cxx b/src/sys/options.cxx index ddbaf8e793..3d6f6a00b4 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -747,9 +747,9 @@ Array Options::as>(const Array& similar_to) Array result = bout::utils::visit( ConvertContainer>{ - fmt::format(fmt::runtime( - _("Value for option {:s} cannot be converted to an Array") - ), + fmt::format( + fmt::runtime( + _("Value for option {:s} cannot be converted to an Array")), full_name), similar_to}, value);