refactor: Check translatable format strings at compile-time #31061

pull maflcko wants to merge 4 commits into bitcoin:master from maflcko:2410-trans changing 15 files +111 −458
  1. maflcko commented at 11:35 am on October 9, 2024: member

    All translatable format strings are fixed. This change surfaces errors in them at compile-time.

    The implementation achieves this by allowing to delay the translation (or std::string construction) that previously happened in _() by returning a new type from this function. The new type can be converted to bilingual_str where needed.

    This can be tested by adding a format string error in an original string literal and observing a new compile-time failure.

    Fixes https://github.com/bitcoin/bitcoin/issues/30530

  2. DrahtBot commented at 11:35 am on October 9, 2024: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31061.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK hodlinator, ryanofsky
    Concept ACK stickies-v
    Stale ACK l0rinc

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31375 (multiprocess: Add bitcoin wrapper executable by ryanofsky)
    • #31296 (wallet: Translate [default wallet] string in progress messages by ryanofsky)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #30221 (wallet: Ensure best block matches wallet scan state by achow101)
    • #28710 (Remove the legacy wallet and BDB dependency by achow101)
    • #26022 (Add util::ResultPtr class by ryanofsky)
    • #25722 (refactor: Use util::Result class for wallet loading by ryanofsky)
    • #25665 (refactor: Add util::Result failure values, multiple error and warning messages by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. DrahtBot renamed this:
    refactor: Check original (translatable) format string at compile-time
    refactor: Check original (translatable) format string at compile-time
    on Oct 9, 2024
  4. DrahtBot added the label Refactoring on Oct 9, 2024
  5. maflcko renamed this:
    refactor: Check original (translatable) format string at compile-time
    refactor: Check translatable format strings at compile-time
    on Oct 9, 2024
  6. in src/clientversion.cpp:74 in fa22616839 outdated
    70@@ -71,7 +71,7 @@ std::string FormatSubVersion(const std::string& name, int nClientVersion, const
    71 
    72 std::string CopyrightHolders(const std::string& strPrefix)
    73 {
    74-    const auto copyright_devs = strprintf(_(COPYRIGHT_HOLDERS).translated, COPYRIGHT_HOLDERS_SUBSTITUTION);
    75+    const auto copyright_devs = strprintf(_(COPYRIGHT_HOLDERS), COPYRIGHT_HOLDERS_SUBSTITUTION).translated;
    


    ryanofsky commented at 12:04 pm on October 9, 2024:

    In commit “refactor: Pick translated string after format” (fa22616839903369757069d50aa8f55e246b2b6c)

    Commit message says this change is “required for a future commit” but that makes it sound this is because of some implementation detail, when I think the actual reason is that strprintf can’t check format string at compile time if it is passed a runtime std::string value.

    Unless I’m missing something would be clearer if commit message just said this passes return value of the underscore function to directly to strprintf so the format string can be checked at compile time.


    maflcko commented at 1:36 pm on October 9, 2024:
    Thanks, fixed commit message.
  7. in src/util/string.h:22 in fa6cef7cf7 outdated
    17@@ -18,6 +18,48 @@
    18 #include <vector>
    19 
    20 namespace util {
    21+template <unsigned num_params>
    22+constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
    


    ryanofsky commented at 12:08 pm on October 9, 2024:

    In commit “move-only: Detail_CheckNumFormatSpecifiers and G_TRANSLATION_FUN” (fa6cef7cf762a33755697dff20b75a9f235e96a9)

    I think it would make more sense to call this function util::detail::CheckNumFormatSpecifiers instead of util::Detail_CheckNumFormatSpecifiers


    maflcko commented at 1:28 pm on October 9, 2024:
    I don’t think this is possible with a simple diff touching only lines that are already touched in this pull request. If you disagree, I am happy to accept any commit that compiles. I am also happy to review any pull request that achieves this and is split up from this pull request.

    ryanofsky commented at 5:37 pm on October 9, 2024:

    re: #31061 (review)

    I don’t think this is possible with a simple diff touching only lines that are already touched in this pull request. If you disagree, I am happy to accept any commit that compiles. I am also happy to review any pull request that achieves this and is split up from this pull request.

    I see what you mean: it’s not possible to introduce a util::detail namespace because it would conflict with the util::detail in the hex_literals inline namespace.

    However, it doesn’t actually make sense for there to be a detail namespace in an inline namespace, and this seemslikely to create problems for callers using the inline namespace, since they may have detail namespaces of their own. This was easy enough to fix so I just fixed it in the suggested branch.


    maflcko commented at 8:17 am on October 10, 2024:
    Thanks, replaced my commit with your 71d382650b5a0bfb21085e8f6bd22152d2148459
  8. in src/test/util_string_tests.cpp:20 in fa6cef7cf7 outdated
    16@@ -17,12 +17,12 @@ template <unsigned NumArgs>
    17 inline void PassFmt(util::ConstevalFormatString<NumArgs> fmt)
    18 {
    19     // This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
    20-    decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
    21+    util::Detail_CheckNumFormatSpecifiers<NumArgs>(fmt.fmt);
    


    ryanofsky commented at 12:11 pm on October 9, 2024:

    In commit “move-only: Detail_CheckNumFormatSpecifiers and G_TRANSLATION_FUN” (fa6cef7cf762a33755697dff20b75a9f235e96a9)

    Is this comment accurate? If so it seems like if is a more obvious approach to avoiding -Wunused errors would be to write (void)fmt; or just drop the fmt variable and declare the function as void PassFmt(util::ConstevalFormatString<NumArgs>)


    maflcko commented at 1:36 pm on October 9, 2024:
    Thanks, added unrelated change to fix comment.

    ryanofsky commented at 5:38 pm on October 9, 2024:

    re: #31061 (review)

    Thanks, added unrelated change to fix comment.

    Thanks, comment makes sense. I knew there was probably some reason for doing it that way but couldn’t figure out what it was.

  9. maflcko force-pushed on Oct 9, 2024
  10. in src/common/init.cpp:110 in fa213aa88d outdated
    106@@ -107,7 +107,7 @@ std::optional<ConfigError> InitConfig(ArgsManager& args, SettingsAbortFn setting
    107             }
    108         }
    109     } catch (const std::exception& e) {
    110-        return ConfigError{ConfigStatus::FAILED, Untranslated(e.what())};
    111+        return ConfigError{ConfigStatus::FAILED, Untranslated(std::string{e.what()})};
    


    ryanofsky commented at 12:18 pm on October 9, 2024:

    In commit “refactor: Mark run-time Untranslated() c_str with std::string” (fa213aa88dfe9354c314a855c72ea33b3966c5c0)

    I think this change is unfortunate and doesn’t make sense. It seems like it could be a avoided with a small scripted diff to replace strprintf(Untranslated("..."), ...) with Untranslated(strprintf("...", ...)) and that replacement would make the code clearer and more consistent anyway.


    maflcko commented at 1:30 pm on October 9, 2024:

    It seems like it could be a avoided with a small scripted diff to replace strprintf(Untranslated("..."), ...) with Untranslated(strprintf("...", ...)) and that replacement would make the code clearer and more consistent anyway.

    Are you sure? The two are not equivalent, because args can also be a bilingual_str, in which case the scripted-diff does not compile. In any case, it seems unrelated to this pull request and I don’t see how it “simplifies” stuff and it touches different lines anyway, so maybe a separate issue or pull request would be better to explore this?

    I think this change is unfortunate and doesn’t make sense.

    Can you explain which part doesn’t make sense and how this relates to strprintf and tinyformat at all? I can’t see any tfm use in the line you commented on.


    ryanofsky commented at 4:38 pm on October 9, 2024:

    re: #31061 (review)

    Thanks, it’s a good point that Untranslated(strprintf) is not equivalent to strprintf(Untranslated) in all cases. But I think those cases are basically broken because they are substituting non-english string fragments inside english strings.

    I created a branch which removes these (4) cases and adds a scripted-diff to remove the remaining strprintf(Untranslated) uses. This allows a lot of simplifications to this PR:

    • Untranslated function is simpler. Has a single definition, and is not overloaded to return different things depending on whether it is passed a std:::string or a c string.
    • “Original” struct no longer requires a template parameter, it can be a simple struct.
    • This commit can be dropped, the clang tidy commit can be dropped, and there’s no need to convert add manual conversions from Untranslated to bilingual_str next commit.

    Branch is https://github.com/ryanofsky/bitcoin/commits/pr/tfmt, commits are:

    • ab4e567f4c8fc2b332e4f620567fab01ede4e1f4 refactor: Don’t embed translated strings in untranslated strings.
    • d5a8e2b24ff46e0bc5e0fdc733663f5c5527efb0 scripted-diff: Replace strprintf(Untranslated) with Untranslated(strprintf)
    • 5e0b4f9df2786ca674df18bd4f8397e3189684be refactor: Check wallet format strings at compile time
    • 6183767bd30494876064fc0ffb1a69269d102b5b refactor: Pick translated string after format
    • 71d382650b5a0bfb21085e8f6bd22152d2148459 move-only: Detail_CheckNumFormatSpecifiers and G_TRANSLATION_FUN
    • 86ea5847925e9baff5c1f02976604a65770e2a72 refactor: Delay translation of Untranslated() or _() literals
    • 08bab0447027ace65f18a72c98b1c87683f8f378 refactor: Check translatable format strings at compile-time

    maflcko commented at 8:23 am on October 10, 2024:

    But I think those cases are basically broken because they are substituting non-english string fragments inside english strings.

    I am not sure I agree. Untranslated("%s:%i ") doesn’t look English to me. To me, it looks like Untranslated() is sometimes used to annotate format strings that are valid in any language and do not need to be translated.

    And your fix doesn’t prevent non-English fragments in English strings anyway, because you are using operator+ to concatenate the English/Untranslated string with a translated (_()) string.

    So I am not sure your suggestion is easy for developers to follow (having to sometimes emulate formatting with operator+ and conversions, and other times not), but maybe I am missing something.


    maflcko commented at 11:45 am on October 10, 2024:

    To extend a bit: Previously (and after my pull right now), it was easy to toggle between translation and non-translation by replacing _ with Untranslated (or vice-versa):

    0-strprintf(Untranslated("Cannot create database file %s"), filename);
    1+strprintf(           _("Cannot create database file %s"), filename);
    

    With your change, the function call order has to be changed as well.


    ryanofsky commented at 12:54 pm on October 11, 2024:

    re: #31061 (review)

    I don’t see ability to swap _ and Untranslated as a real benefit here, because it is pretty easy to switch between translated and non translated format strings either way, and if you are doing that it probably makes sense to actually look at format string arguments and see if they should be translated or untranslated for consistency. The PR also already loses the ability to swap between _ and Untranslated in other places anyway since bilingual_str and Translatable structs are incompatible.

    You are right that Untranslated("%s:%i ") is not an english string and the commit message in ab4e567f4c8fc2b332e4f620567fab01ede4e1f4 wasn’t doing an a good job of describing changes there. I think most of the 54 cases changed in d5a8e2b24ff46e0bc5e0fdc733663f5c5527efb0 are untranslated English strings though, and having a compile time check to make sure non-english strings aren’t inserted accidentally inside them is a good thing.

    If you really disagree with this and prefer strprintf(Untranslated(…)) over Untranslated(strprintf(…)) I think we could also go in that direction with the opposite scripted-diff. I just think trying to support both and making Untranslated function return different things when it is called with a char* instead of std::string, and having to add extra casts here to work around that in this commit is unfortunate.

    It seems like implementation of this PR could be simpler, API can be simpler, and code can be more consistent by having Untranslated return one thing or the other instead of both.


    ryanofsky commented at 1:46 am on October 12, 2024:

    re: #31061 (review)

    Opened #31072 to implement suggested simplification. Marking this thread resolved since it it’s not meant to block this PR in any case.


    maflcko commented at 4:32 pm on December 6, 2024:
    Thanks. Now that it is merged, I went ahead and pushed commit 08bab0447027ace65f18a72c98b1c87683f8f378 from #31061 (review) into this pull (rebased)
  11. in src/test/result_tests.cpp:93 in aaaa4fb201 outdated
    88@@ -89,8 +89,8 @@ BOOST_AUTO_TEST_CASE(check_value_or)
    89     BOOST_CHECK_EQUAL(IntFn(10, false).value_or(20), 20);
    90     BOOST_CHECK_EQUAL(NoCopyFn(10, true).value_or(20), 10);
    91     BOOST_CHECK_EQUAL(NoCopyFn(10, false).value_or(20), 20);
    92-    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), true).value_or(Untranslated("B")), Untranslated("A"));
    93-    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), Untranslated("B"));
    94+    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), true).value_or(Untranslated("B")), bilingual_str{Untranslated("A")});
    95+    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), bilingual_str{Untranslated("B")});
    


    ryanofsky commented at 12:32 pm on October 9, 2024:

    In commit “refactor: Delay translation of Untranslated() or _() literals” (aaaa4fb20156b4375d92e1eca4acc90a425a1896)

    Looks like these changes would also not be necessary if using earlier suggestion to replace strprintf(Untranslated("..."), ...) with Untranslated(strprintf("...", ...)) so Untranslated definition does not have to change.


    maflcko commented at 1:30 pm on October 9, 2024:
    (same)
  12. in src/util/translation.h:29 in fac722986e outdated
    24+{
    25+    return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
    26+}
    27+/** Type to denote whether an orginal string literal is translatable */
    28+template <bool translatable = true>
    29+struct Original {
    


    ryanofsky commented at 12:45 pm on October 9, 2024:

    In commit “refactor: Delay translation of Untranslated() or _() literals” (aaaa4fb20156b4375d92e1eca4acc90a425a1896)

    Name Original is vague. Would suggest renaming Original something like bilingual_literal or bilingual_const to be consistent with bilingual_str and bilingual_fmt since it represents a bilingual literal or constant string which is the return value of the underscore function.

    I also think this can just be a normal non-template struct, and bool translatable template argument can be dropped if you take eariler suggestion to replace strprintf(Untranslated("..."), ...) with Untranslated(strprintf("...", ...)) so Untranlated can continue to return bilingual_str


    maflcko commented at 8:26 am on October 10, 2024:
    Ok, renamed to Translatable<bool _>. I don’t think bilingual_* makes sense, because it is only an internally used type to hold the hand of the compiler. The type is supposed to convert into a real bilingual_str either directly or via bilingual_fmt and tfm.
  13. maflcko force-pushed on Oct 9, 2024
  14. DrahtBot added the label CI failed on Oct 9, 2024
  15. DrahtBot commented at 12:46 pm on October 9, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/31292791221

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  16. in src/util/translation.h:87 in aaaa4fb201 outdated
    72@@ -47,8 +73,11 @@ inline bilingual_str operator+(bilingual_str lhs, const bilingual_str& rhs)
    73     return lhs;
    74 }
    75 
    76+consteval auto _(util::Original<true> str) { return str; }
    


    ryanofsky commented at 12:47 pm on October 9, 2024:

    In commit “refactor: Delay translation of Untranslated() or _() literals” (aaaa4fb20156b4375d92e1eca4acc90a425a1896)

    Should be no need for this if take earlier suggestion to replace strprintf(Untranslated("..."), ...) with Untranslated(strprintf("...", ...))

  17. in src/util/translation.h:27 in fabb28f6c8 outdated
    22+ */
    23+inline std::string translate(const char* lit)
    24+{
    25+    return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
    26+}
    27+/** Type to denote whether an orginal string literal is translatable */
    


    stickies-v commented at 12:54 pm on October 9, 2024:

    typo nit

    0/** Type to denote whether an original string literal is translatable */
    

    maflcko commented at 8:27 am on October 10, 2024:
    thanks, fixed
  18. in src/util/translation.h:60 in aaaa4fb201 outdated
    40@@ -22,6 +41,13 @@ struct bilingual_str {
    41     std::string original;
    42     std::string translated;
    43 
    44+    bilingual_str() = default;
    45+    bilingual_str(std::string original, std::string translated) : original{original}, translated{translated} {}
    46+    template <bool translatable>
    47+    bilingual_str(util::Original<translatable> original) : original{original.lit}, translated{original.translate()}
    48+    {
    49+    }
    


    ryanofsky commented at 12:54 pm on October 9, 2024:

    In commit “refactor: Delay translation of Untranslated() or _() literals” (aaaa4fb20156b4375d92e1eca4acc90a425a1896)

    I think it confuses and complicates the design for bilingual_str to be aware of Original since bilingual_str is just a simple, low-level pair of strings that shouldn’t know about formatting.

    I think we do need return value of underscore function to be implicitly convertible to bilingual_str but that would be more cleanly implemented by adding an operator bilingual_str to the Original class.

    Also (not sure about this, but) it might be nice to add an explicit conversion method to Original class, so we could write something like _("Error opening block database").str() instead of bilingual_str{_("Error opening block database")}


    maflcko commented at 2:32 pm on October 9, 2024:

    Also (not sure about this, but) it might be nice to add an explicit conversion method to Original class, so we could write something like _("Error opening block database").str() instead of bilingual_str{_("Error opening block database")}

    Sure, I am happy to add bilingual_str str() and operator bilingual_str() to the Original type and remove the constructors from bilingual_str. However, this means that calling bilingual_str{Original{}} is now forbidden and one would have to use () over {}, or call the .str() method.

    Let me know if this is fine and I should go ahead and push this diff:

      0diff --git a/src/init.cpp b/src/init.cpp
      1index fb2028281a..c849f83b05 100644
      2--- a/src/init.cpp
      3+++ b/src/init.cpp
      4@@ -1261,7 +1261,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
      5             return f();
      6         } catch (const std::exception& e) {
      7             LogError("%s\n", e.what());
      8-            return std::make_tuple(node::ChainstateLoadStatus::FAILURE, bilingual_str{_("Error opening block database")});
      9+            return std::make_tuple(node::ChainstateLoadStatus::FAILURE, _("Error opening block database").str());
     10         }
     11     };
     12     auto [status, error] = catch_exceptions([&] { return LoadChainstate(chainman, cache_sizes, options); });
     13diff --git a/src/test/result_tests.cpp b/src/test/result_tests.cpp
     14index 45337c6801..c829d56faa 100644
     15--- a/src/test/result_tests.cpp
     16+++ b/src/test/result_tests.cpp
     17@@ -63,7 +63,7 @@ void ExpectSuccess(const util::Result<T>& result, const bilingual_str& str, Args
     18 {
     19     ExpectResult(result, true, str);
     20     BOOST_CHECK_EQUAL(result.has_value(), true);
     21-    BOOST_CHECK_EQUAL(result.value(), T{std::forward<Args>(args)...});
     22+    BOOST_CHECK_EQUAL(result.value(), T(std::forward<Args>(args)...));
     23     BOOST_CHECK_EQUAL(&result.value(), &*result);
     24 }
     25 
     26@@ -89,8 +89,8 @@ BOOST_AUTO_TEST_CASE(check_value_or)
     27     BOOST_CHECK_EQUAL(IntFn(10, false).value_or(20), 20);
     28     BOOST_CHECK_EQUAL(NoCopyFn(10, true).value_or(20), 10);
     29     BOOST_CHECK_EQUAL(NoCopyFn(10, false).value_or(20), 20);
     30-    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), true).value_or(Untranslated("B")), bilingual_str{Untranslated("A")});
     31-    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), bilingual_str{Untranslated("B")});
     32+    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), true).value_or(Untranslated("B")), Untranslated("A").str());
     33+    BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), Untranslated("B").str());
     34 }
     35 
     36 BOOST_AUTO_TEST_SUITE_END()
     37diff --git a/src/util/translation.h b/src/util/translation.h
     38index 08df892733..a06f7b982f 100644
     39--- a/src/util/translation.h
     40+++ b/src/util/translation.h
     41@@ -14,24 +14,6 @@
     42 /** Translate a message to the native language of the user. */
     43 const extern std::function<std::string(const char*)> G_TRANSLATION_FUN;
     44 
     45-namespace util {
     46-/**
     47- * Translation function.
     48- * If no translation function is set, simply return the input.
     49- */
     50-inline std::string translate(const char* lit)
     51-{
     52-    return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
     53-}
     54-/** Type to denote whether an orginal string literal is translatable */
     55-template <bool translatable = true>
     56-struct Original {
     57-    const char* const lit;
     58-    consteval Original(const char* str) : lit{str} { assert(lit); }
     59-    std::string translate() const { return translatable ? util::translate(lit) : lit; }
     60-};
     61-} // namespace util
     62-
     63 /**
     64  * Bilingual messages:
     65  *   - in GUI: user's native language + untranslated (i.e. English)
     66@@ -41,13 +23,6 @@ struct bilingual_str {
     67     std::string original;
     68     std::string translated;
     69 
     70-    bilingual_str() = default;
     71-    bilingual_str(std::string original, std::string translated) : original{original}, translated{translated} {}
     72-    template <bool translatable>
     73-    bilingual_str(util::Original<translatable> original) : original{original.lit}, translated{original.translate()}
     74-    {
     75-    }
     76-
     77     bilingual_str& operator+=(const bilingual_str& rhs)
     78     {
     79         original += rhs.original;
     80@@ -73,6 +48,26 @@ inline bilingual_str operator+(bilingual_str lhs, const bilingual_str& rhs)
     81     return lhs;
     82 }
     83 
     84+namespace util {
     85+/**
     86+ * Translation function.
     87+ * If no translation function is set, simply return the input.
     88+ */
     89+inline std::string translate(const char* lit)
     90+{
     91+    return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
     92+}
     93+/** Type to denote whether an orginal string literal is translatable */
     94+template <bool translatable = true>
     95+struct Original {
     96+    const char* const lit;
     97+    consteval Original(const char* str) : lit{str} { assert(lit); }
     98+    std::string translate() const { return translatable ? util::translate(lit) : lit; }
     99+    bilingual_str str() const { return {lit, translate()}; }
    100+    operator bilingual_str() const { return str(); }
    101+};
    102+} // namespace util
    103+
    104 consteval auto _(util::Original<true> str) { return str; }
    105 
    106 /** Mark a bilingual_str as untranslated */
    

    ryanofsky commented at 6:04 pm on October 9, 2024:

    re: #31061 (review)

    Yes I do think that’s better and basically made the same change in my branch, though without the str() method since it seems it makes it a little less clear what return type is.


    maflcko commented at 8:27 am on October 10, 2024:

    Yes I do think that’s better and basically made the same change in my branch, though without the str() method since it seems it makes it a little less clear what return type is.

    Ok, pushed my diff above, with the added str() removed again.

  19. in src/util/translation.h:29 in aaaa4fb201 outdated
    23+{
    24+    return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
    25+}
    26+/** Type to denote whether an orginal string literal is translatable */
    27+template <bool translatable = true>
    28+struct Original {
    


    stickies-v commented at 1:02 pm on October 9, 2024:
    in aaaa4fb20156b4375d92e1eca4acc90a425a1896: seems more straightforward to just extend ConstevalStringLiteral instead of creating a new util::Original? This would 1) minimize the diff and 2) I find ConstevalStringLiteral a much more helpful name than Original.

    ryanofsky commented at 6:19 pm on October 9, 2024:

    re: #31061 (review)

    in aaaa4fb: seems more straightforward to just extend ConstevalStringLiteral instead of creating a new util::Original? This would 1) minimize the diff and 2) I find ConstevalStringLiteral a much more helpful name than Original.

    I think this is only a question of naming since Original class is replacing ConstevalStringLiteral this commit. In by branch I called this ConstEvalTranslated, since it’s the type name for values returned from the underscore function, and is not just referring to a generic string literal. But I didn’t put much thought into the name and something else may be better.


    maflcko commented at 8:32 am on October 10, 2024:

    in aaaa4fb: seems more straightforward to just extend ConstevalStringLiteral instead of creating a new util::Original? This would 1) minimize the diff and 2) I find ConstevalStringLiteral a much more helpful name than Original.

    Are you sure it would minimize the diff? Original needs to deal with the concept of translations, which seems out-of-scope for ConstevalStringLiteral. Having two separate types to only deal with the things they care about seems easier. However, I may be missing something and if you find a smaller diff, I am happy to consider it.

    I’ve renamed the type to Translatable<bool _>.

  20. ryanofsky approved
  21. ryanofsky commented at 1:02 pm on October 9, 2024: contributor
    Code review ACK fabb28f6c8f772d38917a23dfbda706292070ba1. This looks great, and implementation is very clean. However, I think it could be simplified significantly if there were a scripted diff commit to replace strprintf(Untranslated("..."), ...) with Untranslated(strprintf("...", ...)), which would be a good change on its own for clarity and consistency. More details in comments below.
  22. maflcko force-pushed on Oct 9, 2024
  23. DrahtBot removed the label CI failed on Oct 9, 2024
  24. in src/qt/walletcontroller.cpp:434 in fad211bfef outdated
    430@@ -431,7 +431,7 @@ void RestoreWalletActivity::finish()
    431         QMessageBox::warning(m_parent_widget, tr("Restore wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
    432     } else {
    433         //: Title of message box which is displayed when the wallet is successfully restored.
    434-        QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString(Untranslated("Wallet restored successfully \n").translated));
    435+        QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString(Untranslated("Wallet restored successfully \n").translate()));
    


    stickies-v commented at 3:10 pm on October 9, 2024:
    0        QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString("Wallet restored successfully \n"));
    

    ryanofsky commented at 6:12 pm on October 9, 2024:

    re: #31061 (review)

    Would be reasonable to change this but no more change is actually required here with my suggested branch.


    ryanofsky commented at 1:52 am on October 12, 2024:

    re: #31061 (review)

    Simplified this further in 6fb39655c5cd2bebad902d271d1edae64b817d2b from #31072. It might just be a bug that this string is not translated, and there also seems to be trailing whitespace in the string. Current commit just simplifies code and keeps existing behavior though.


    maflcko commented at 5:44 pm on October 24, 2024:
    I don’t like this suggestion. If you disagree, you can continue discussion in #31072 (review)

    maflcko commented at 4:34 pm on December 6, 2024:
    The hunk isn’t needed anymore, so I dropped it.
  25. in src/util/translation.h:42 in fad211bfef outdated
    37+    const bool translatable;
    38+    template <bool translatable>
    39+    consteval bilingual_fmt(Original<translatable> o) : original{o.lit}, translatable{translatable}
    40+    {
    41+    }
    42+    std::string translate() { return translate(original.fmt); }
    


    stickies-v commented at 3:34 pm on October 9, 2024:

    This function is currently unused, and it seems incorrectly implemented as it doesn’t compile when actually used:

    0In file included from ../../src/clientversion.cpp:9:
    1../../src/util/translation.h:42:48: error: too many arguments to function call, expected 0, have 1
    2    std::string translate() { return translate(original.fmt); }
    3                                     ~~~~~~~~~ ^~~~~~~~~~~~
    4../../src/util/translation.h:106:42: note: in instantiation of member function 'util::bilingual_fmt<1>::translate' requested here
    5                         tfm::format(fmt.translate(), translate_arg(args, true)...)};
    6                                         ^
    7../../src/clientversion.cpp:74:33: note: in instantiation of function template specialization 'tinyformat::format<char[13]>' requested here
    8    const auto copyright_devs = strprintf(_(COPYRIGHT_HOLDERS), COPYRIGHT_HOLDERS_SUBSTITUTION).translated;
    

    Suggested fix to address both:

     0diff --git a/src/util/translation.h b/src/util/translation.h
     1index 0f230017d9..2de99f386d 100644
     2--- a/src/util/translation.h
     3+++ b/src/util/translation.h
     4@@ -39,7 +39,7 @@ struct bilingual_fmt {
     5     consteval bilingual_fmt(Original<translatable> o) : original{o.lit}, translatable{translatable}
     6     {
     7     }
     8-    std::string translate() { return translate(original.fmt); }
     9+    std::string translate() { return util::translate(original.fmt); }
    10 };
    11 } // namespace util
    12 
    13@@ -103,7 +103,7 @@ bilingual_str format(util::bilingual_fmt<sizeof...(Args)> fmt, const Args&... ar
    14         }
    15     }};
    16     return bilingual_str{tfm::format(fmt.original, translate_arg(args, false)...),
    17-                         tfm::format(util::translate(fmt.original.fmt), translate_arg(args, true)...)};
    18+                         tfm::format(fmt.translate(), translate_arg(args, true)...)};
    19 }
    20 } // namespace tinyformat
    21 
    

    ryanofsky commented at 6:11 pm on October 9, 2024:

    re: #31061 (review)

    This is a good catch, but it seems better to just drop the template method instead of trying to fix it up and use it. It just seems to add complexity and it doesn’t seem like there is another use-case for it.


    maflcko commented at 9:20 am on October 10, 2024:

    Suggested fix to address both:

    Good catch. Though, I don’t think the fix fixes all issues. There was a third that the translatable member field was unused. I’ve pushed a fix to fix all three issues.

  26. in src/util/translation.h:28 in fad211bfef outdated
    23+inline std::string translate(const char* lit)
    24+{
    25+    return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
    26+}
    27+/** Type to denote whether an orginal string literal is translatable */
    28+template <bool translatable = true>
    


    stickies-v commented at 4:33 pm on October 9, 2024:
    nit: I would prefer templating this on an enum {Translateable,Untranslateable} for readability over the non-obvious <{true,false}> boolean

    ryanofsky commented at 6:08 pm on October 9, 2024:

    nit: I would prefer templating this on an enum {Translateable,Untranslateable} for readability over the non-obvious <{true,false}> boolean

    Note: with changes in suggested branch it should no longer be necessary for this to be templated.


    maflcko commented at 8:33 am on October 10, 2024:

    I’ve renamed the type to Translatable<bool _>, which should clear this up without adding an enum?

    The type is only supposed to be used internally, so I can also wrap it into a detail:: if you think this is helpful?

  27. stickies-v commented at 4:36 pm on October 9, 2024: contributor

    Concept ACK, and this is a smaller diff than I’d expected to implement this, nice!

    I need to think about it more and work out some code, but I feel like the new interface is not as intuitive yet as it could be. Will follow up with that in next review.

  28. jarolrod commented at 3:06 am on October 10, 2024: member
    concept ack
  29. maflcko force-pushed on Oct 10, 2024
  30. maflcko force-pushed on Oct 10, 2024
  31. in src/util/translation.h:81 in fa62737ae3 outdated
    65+    std::string translate() const { return translatable ? util::translate(lit) : lit; }
    66+    operator bilingual_str() const { return {lit, translate()}; }
    67+};
    68+} // namespace util
    69+
    70+consteval auto _(util::Translatable<true> str) { return str; }
    


    ryanofsky commented at 1:40 pm on October 11, 2024:

    In commit “refactor: Delay translation of Untranslated() or _() literals” (fa62737ae398bd56ef9e68b8d0a4d217cfcc532b)

    Thinking about this more it seems like _ function does not be consteval, and could just return a template type that inherits from bilingual_str, and includes the number of format specifiers included in the string as part of its type information. This would be nice because runtime semantics of _ would be unchanged, it would still do the translation at time it is called, and there would be no need to make any translated -> translate() replacements.

    I think the main downside to this approach is would be need to refactor CheckNumFormatSpecifiers but there would be fewer changes overall. The other downside would be that _ would need to be aware the translated strings might be a format string and count the specifiers instead of just passing it along blindly. Neither downside would have to exist if c++ just allowed arguments to consteval functions to be treated as compile time constants, which they are by definition, but they did not do this (I think because stylistically they want to see compile-time arguments passed as <> arguments not () arguments.)

    Not sure about any of this, though, would need to experiment.


    ryanofsky commented at 7:39 pm on October 11, 2024:

    re: #31061 (review)

    Not sure about any of this, though, would need to experiment.

    To follow up, I was able to implement an alternate approach in #31074 that lets _ and Untranslated always return bilingual_str instances that you can access with .original and .translated instead of .lit and translate(). The resulting API seems more clean and consistent, but it does require messier syntax when calling strprintf (implmented in a scripted diff).

    I’m not really sure about tradeoffs of messier syntax vs cleaner API, and I’m guessing there’s some other way to make the API more consistent.

  32. ryanofsky approved
  33. ryanofsky commented at 1:53 pm on October 11, 2024: contributor

    Code review ACK fae6acacc0216aa07f5a4a0ee75fbade620d7023. This is a nice improvement overall, and definitely better than the status quo due to extra type checking it provides. I do think there is extra complexity here and extra changes that could go away if we adopt #31072 and stop passing untranslated format strings to strprintf.

    I also think it probably would be nicer if _() would continue to look up translations right away, instead of delaying them until conversion to bilingual str. I think it could return a bilingual_str with extra compile-time information instead of returning different type with .lit and .translate() fields. I’m not sure this is possible though, and it’s likely there’s something I’m overlooking.

    EDIT: I am implemented an approach in #31074 that makes _() and Untranslated() both return bilingual_str with .original and .translated fields and no not delay the time of translation. Unfortunately, because C++ does not currently allow marking function parameters constexpr, it means strings needs to be passed as template parameters instead of function parameters, resulting in a messier syntax, even though the implementation and API are simpler. I closed the PR because it is not a clear win over this one.

  34. DrahtBot requested review from stickies-v on Oct 11, 2024
  35. ryanofsky referenced this in commit 6fb39655c5 on Oct 12, 2024
  36. DrahtBot added the label Needs rebase on Oct 24, 2024
  37. maflcko force-pushed on Oct 24, 2024
  38. maflcko marked this as a draft on Oct 24, 2024
  39. DrahtBot removed the label Needs rebase on Oct 24, 2024
  40. DrahtBot added the label Needs rebase on Oct 28, 2024
  41. maflcko force-pushed on Oct 29, 2024
  42. DrahtBot removed the label Needs rebase on Oct 29, 2024
  43. ryanofsky referenced this in commit 49d4032d2d on Oct 29, 2024
  44. maflcko force-pushed on Nov 13, 2024
  45. fanquake referenced this in commit f44e39c9d0 on Nov 14, 2024
  46. DrahtBot added the label Needs rebase on Nov 14, 2024
  47. maflcko force-pushed on Nov 14, 2024
  48. DrahtBot removed the label Needs rebase on Nov 14, 2024
  49. l0rinc commented at 3:00 pm on November 14, 2024: contributor
    Concept ACK, nice to see these verifications being done earlier
  50. ryanofsky approved
  51. ryanofsky commented at 2:44 am on November 15, 2024: contributor

    Code review ACK 2c15a858ca7b3f14eaebf4b39134cd0e9c559273. No changes since the last review other than rebase.

    I do think it would simplify this PR and make review more meaningful if #31072 could be merged first. This PR is combining application code changes which require looking at surrounding code and seeing if the changes fit in, with translation class changes which require thinking about API design. I think it’s harder to evaluate both things clearly when they are bundled in the same PR. I wouldn’t object to this PR going in first though since it has been open longer.

  52. DrahtBot requested review from l0rinc on Nov 15, 2024
  53. ryanofsky referenced this in commit 1f29e5d5fc on Nov 15, 2024
  54. ryanofsky referenced this in commit ec038f52ab on Nov 15, 2024
  55. DrahtBot added the label Needs rebase on Nov 15, 2024
  56. maflcko force-pushed on Nov 15, 2024
  57. ryanofsky referenced this in commit 9120245e76 on Nov 15, 2024
  58. ryanofsky referenced this in commit b8bf7bf55b on Nov 15, 2024
  59. ryanofsky referenced this in commit 5c6a94bba0 on Nov 15, 2024
  60. ryanofsky referenced this in commit 222ced7d2f on Nov 15, 2024
  61. ryanofsky approved
  62. ryanofsky commented at 4:49 pm on November 15, 2024: contributor
    Code review ACK f4df783f1ca22d96476d52ec5d1929547691ba13. Just rebased and reordered commit since last review
  63. DrahtBot removed the label Needs rebase on Nov 15, 2024
  64. l0rinc commented at 6:50 pm on November 20, 2024: contributor
    utACK f4df783f1ca22d96476d52ec5d1929547691ba13
  65. DrahtBot added the label CI failed on Nov 22, 2024
  66. DrahtBot removed the label CI failed on Nov 22, 2024
  67. in src/util/translation.h:77 in f4df783f1c outdated
    72+    const bool translatable;
    73+    template <bool translatable>
    74+    consteval bilingual_fmt(Translatable<translatable> o) : original{o.lit}, translatable{translatable}
    75+    {
    76+    }
    77+    std::string translate() { return translatable ? util::translate(original.fmt) : original.fmt; }
    


    hodlinator commented at 8:29 am on December 4, 2024:

    nit:

    0    std::string translate() const { return translatable ? util::translate(original.fmt) : original.fmt; }
    

    maflcko commented at 4:36 pm on December 6, 2024:
    removed the function instead
  68. in src/test/translation_tests.cpp:15 in f4df783f1c outdated
    11@@ -12,10 +12,10 @@ BOOST_AUTO_TEST_SUITE(translation_tests)
    12 BOOST_AUTO_TEST_CASE(translation_namedparams)
    13 {
    14     bilingual_str arg{"original", "translated"};
    15-    bilingual_str format{"original [%s]", "translated [%s]"};
    16+    constexpr auto format{Untranslated("original [%s]")};
    


    hodlinator commented at 1:44 pm on December 4, 2024:

    nit: Can’t we just be explicit here in the test to document what is happening, since it is slightly tricky?

    0    constexpr util::Translatable<false> format{Untranslated("original [%s]")};
    

    maflcko commented at 4:35 pm on December 6, 2024:
    I think the type is an implementation detail and doesn’t matter, as long as it compiles. One could even inline this line into the next line, but I wanted to keep the diff minimal.
  69. hodlinator approved
  70. hodlinator commented at 2:15 pm on December 4, 2024: contributor

    ACK f4df783f1ca22d96476d52ec5d1929547691ba13

    Increased validation of format strings. :+1:


    Maybe amend PR summary or final commit by spelling out something like:

    Delays bilingual_str-construction until the end of the overload of tinyformat::format in translation.h. The fmt parameter to the overload is changed from bilingual_str -> util::bilingual_fmt, the latter being a new type which contains a ConstevalFormatString. bilingual_fmt in turn is only constructible from the new util::Translatable type, which is only returned by calling one of:

    • Translatable<true> _(const char*)
    • Translatable<false> Untranslated(const char*)

    Iff you re-touch, consider making code a bit less template-heavy by splitting Translatable in two:

     0diff --git a/src/util/translation.h b/src/util/translation.h
     1index 2da4f68e55..657cd21f9c 100644
     2--- a/src/util/translation.h
     3+++ b/src/util/translation.h
     4@@ -58,31 +58,32 @@ inline std::string translate(const char* lit)
     5 {
     6     return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(lit) : lit;
     7 }
     8-/** Type to denote whether an original string literal is translatable */
     9-template <bool translatable = true>
    10 struct Translatable {
    11     const char* const lit;
    12     consteval Translatable(const char* str) : lit{str} { assert(lit); }
    13-    std::string translate() const { return translatable ? util::translate(lit) : lit; }
    14+    std::string translate() const { return util::translate(lit); }
    15     operator bilingual_str() const { return {lit, translate()}; }
    16 };
    17+struct Untranslatable {
    18+    const char* const lit;
    19+    consteval Untranslatable(const char* str) : lit{str} { assert(lit); }
    20+    operator bilingual_str() const { return {lit, lit}; }
    21+};
    22 template <unsigned num_params>
    23 struct bilingual_fmt {
    24     const ConstevalFormatString<num_params> original;
    25     const bool translatable;
    26-    template <bool translatable>
    27-    consteval bilingual_fmt(Translatable<translatable> o) : original{o.lit}, translatable{translatable}
    28-    {
    29-    }
    30+    consteval bilingual_fmt(Translatable o) : original{o.lit}, translatable{true} {}
    31+    consteval bilingual_fmt(Untranslatable o) : original{o.lit}, translatable{false} {}
    32     std::string translate() { return translatable ? util::translate(original.fmt) : original.fmt; }
    33 };
    34 } // namespace util
    35
    36-consteval auto _(util::Translatable<true> str) { return str; }
    37+consteval auto _(util::Translatable str) { return str; }
    38
    39 /** Mark a bilingual_str as untranslated */
    40 inline bilingual_str Untranslated(std::string original) { return {original, original}; }
    41-consteval util::Translatable<false> Untranslated(const char* original) { return original; }
    42+consteval util::Untranslatable Untranslated(const char* original) { return original; }
    43
    44 // Provide an overload of tinyformat::format which can take bilingual_str arguments.
    45 namespace tinyformat {
    
  71. ryanofsky referenced this in commit 39950e148d on Dec 4, 2024
  72. maflcko force-pushed on Dec 4, 2024
  73. ryanofsky referenced this in commit 2055acbae9 on Dec 4, 2024
  74. ryanofsky referenced this in commit 058021969b on Dec 4, 2024
  75. hodlinator commented at 10:05 pm on December 4, 2024: contributor
    Should this PR still be in Draft-status @maflcko?
  76. maflcko commented at 11:13 am on December 6, 2024: member
    Looks like #31072 will make it, so I’ll leave this in draft for now.
  77. fanquake referenced this in commit 22723c809a on Dec 6, 2024
  78. DrahtBot added the label Needs rebase on Dec 6, 2024
  79. maflcko force-pushed on Dec 6, 2024
  80. maflcko commented at 4:39 pm on December 6, 2024: member
    addressed feedback, rebased
  81. maflcko marked this as ready for review on Dec 6, 2024
  82. maflcko force-pushed on Dec 6, 2024
  83. DrahtBot removed the label Needs rebase on Dec 6, 2024
  84. in src/util/translation.h:74 in fa6b1d2a26 outdated
    69+template <unsigned num_params>
    70+struct bilingual_fmt {
    71+    const ConstevalFormatString<num_params> original;
    72+    consteval bilingual_fmt(Translatable o) : original{o.lit}
    73+    {
    74+    }
    


    hodlinator commented at 10:00 pm on December 6, 2024:

    nit: Since previous review, this constructors function prototype has shrunk enough to keep it on one line, as you are doing for Translatable just above.

    0    consteval bilingual_fmt(Translatable o) : original{o.lit} {}
    

    maflcko commented at 8:30 am on December 7, 2024:
    thx, clang-formatted
  85. in src/util/translation.h:70 in fa6b1d2a26 outdated
    65+    std::string translate() const { return util::translate(lit); }
    66+    operator bilingual_str() const { return {lit, translate()}; }
    67+};
    68+
    69+template <unsigned num_params>
    70+struct bilingual_fmt {
    


    hodlinator commented at 10:13 pm on December 6, 2024:

    nit: Use Consistent naming convention in newer code as recommended by developer-notes?

    0struct BilingualFmt {
    

    maflcko commented at 8:30 am on December 7, 2024:
    thx, done
  86. hodlinator changes_requested
  87. hodlinator commented at 10:25 pm on December 6, 2024: contributor

    Code reviewed fa6b1d2a26e25e3e6a4ef37835426371001d01ae

    Really nice how you could slim this down after #31072 merged, removing Translatable<false> from this PR as after that PR Untranslated() is no longer sent into strprintf/tfm::format. Now we only need the two Translatable and bilingual_fmt as separate types as a consequence of all _()-calls not being used as format strings.


    Only blocker for me is that the commit message in aaaa7e3f285d5d0ff55df4dfe050334215e2c582 still states:

    This is required for a future commit that requires Untranslated() and _() to be consteval for format literals.

    Untranslated() is not made consteval in this PR.

  88. maflcko force-pushed on Dec 7, 2024
  89. maflcko commented at 10:40 am on December 7, 2024: member
    I figured I can just push the final commit to fix #30530 as well.
  90. in src/tinyformat.h:182 in fab3893fa9 outdated
    178@@ -179,13 +179,19 @@ namespace tfm = tinyformat;
    179 
    180 namespace tinyformat {
    181 
    182+// Similar to std::runtime_format from C++26.
    


    hodlinator commented at 1:04 pm on December 7, 2024:

    nit:

    0+// Added for Bitcoin Core.
    

    Maybe use some comment style to group the modifications to avoid repeating this, similar to https://www.doxygen.nl/manual/grouping.html


    maflcko commented at 10:24 am on December 9, 2024:
    thx, done
  91. in src/tinyformat.h:196 in fab3893fa9 outdated
    190 // formatting without compile time checks.
    191 template <unsigned num_params>
    192 struct FormatStringCheck {
    193     consteval FormatStringCheck(const char* str) : fmt{util::ConstevalFormatString<num_params>{str}.fmt} {}
    194-    FormatStringCheck(const std::string& str) : fmt{str.c_str()} {}
    195+    FormatStringCheck(const RuntimeFormat& run) : fmt{run.fmt.c_str()} {}
    


    hodlinator commented at 1:18 pm on December 7, 2024:

    Just realized there’s some risk of dangling pointers.

    0    FormatStringCheck(const RuntimeFormat& run LIFETIMEBOUND) : fmt{run.fmt.c_str()} {}
    

    Also, if you’re going to rename this one str->run, maybe call the other one comp or something?


    maflcko commented at 10:24 am on December 9, 2024:

    LIFETIMEBOUND

    Added =delete as an alternative instead.


    ryanofsky commented at 10:28 pm on December 9, 2024:

    re: #31061 (review)

    Delete seems good, but I think maybe LIFETIMEBOUND could still be helpful because fmt member here is being assigned the result of run.fmt.c_str(), so the RuntimeFormat object lifetime needs to be at least as long as FormatStringCheck object lifetime, and maybe LIFETIMEBOUND could help enforce this? I don’t know all of the details about how LIFETIMEBOUND works though, so this is just a question.


    maflcko commented at 8:25 am on December 10, 2024:

    Ah, I forgot to remove the & in this line here, for it to have any effect. (force pushed to fix it)

    My understanding is that the only way to construct a RuntimeFormat, whose copy and move ctors are deleted, would be in the FormatStringCheck(RuntimeFormat run) constructor itself. Thus, the string will be part of the same full-expression, and there are no lifetimebound issues coming from the runtime format string.

    Obviously there will still be lifetime issues from the FormatStringCheck, but they exist even in C++26. See https://godbolt.org/z/Y4oxr3jxb . Also, they are not detected by the liftetimebound attribute, currently. My understanding is that temporal safety in C++ is still being worked on.

    If you want, I can delete the copy/move ctors of FormatStringCheck as well, but this seems like a pre-existing issue, which will also be re-introduced when switching to C++26 formatting.


    maflcko commented at 8:27 pm on December 10, 2024:

    Reverted to the previously reviewed version. I don’t think LIFETIMEBOUND is useful here, because:

    • It is still very limited in what it can detect, so adding the include here seems overkill, also given that RuntimeFormat strings should be rare either way, and even rarer for developers to be touched.
    • Anything it detects should be detected by asan as well, so bad code can’t make it in either way.
    • As mentioned in the previous comment, the annotation isn’t present in C++26 right now, either.
    • Once MSVC has fixed their bug to implement C++17 copy elision lifetimes correctly (https://eel.is/c++draft/class.copy.elision#1), this wouldn’t be needed and the code can be changed to fad431e548 again.

    ryanofsky commented at 2:31 am on December 11, 2024:

    re: #31061 (review)

    I’m still not sure I see any downsides to adding LIFETIMEBOUND. It seems like a great way to document the fact that these functions require long-lived references, and to provide instant feedback for code and design bugs without needing to create a PR and wait for an ASAN job to fail.

    I also don’t understand why the additional temporaries and moves and elisions in fad431e548a694600a3fe212127052e5f922a622 would preferable to using references and creating the minimum number of objects with straightforward full-expression lifetimes.

    fad431e548a694600a3fe212127052e5f922a622 also seems unsafe because it doesn’t prevent lvalues from being passed to the FormatStringCheck constructor, and if an lvalue (or any other argument incompatible with copy elision) is passed it will result in FormatStringCheck::fmt being assigned a dangling pointer.

    Would suggest following changes as a simpler alternative:

     0--- a/src/tinyformat.h
     1+++ b/src/tinyformat.h
     2@@ -141,6 +141,7 @@ namespace tfm = tinyformat;
     3 
     4 //------------------------------------------------------------------------------
     5 // Implementation details.
     6+#include <attributes.h> // Added for Bitcoin Core
     7 #include <algorithm>
     8 #include <iostream>
     9 #include <sstream>
    10@@ -181,10 +182,8 @@ namespace tinyformat {
    11 
    12 // Added for Bitcoin Core. Similar to std::runtime_format from C++26.
    13 struct RuntimeFormat {
    14-    std::string fmt; // Not a string view, because tinyformat requires a c_str
    15-    explicit RuntimeFormat(std::string str) : fmt{std::move(str)} {}
    16-    RuntimeFormat(const RuntimeFormat&) = delete;
    17-    RuntimeFormat& operator=(const RuntimeFormat&) = delete;
    18+    const std::string& fmt; // Not a string view, because tinyformat requires a c_str
    19+    explicit RuntimeFormat(LIFETIMEBOUND const std::string& str) : fmt{str} {}
    20 };
    21 
    22 // Added for Bitcoin Core. Wrapper for checking format strings at compile time.
    23@@ -193,7 +192,7 @@ struct RuntimeFormat {
    24 template <unsigned num_params>
    25 struct FormatStringCheck {
    26     consteval FormatStringCheck(const char* str) : fmt{util::ConstevalFormatString<num_params>{str}.fmt} {}
    27-    FormatStringCheck(const RuntimeFormat& run) : fmt{run.fmt.c_str()} {}
    28+    FormatStringCheck(LIFETIMEBOUND const RuntimeFormat& run) : fmt{run.fmt.c_str()} {}
    29     FormatStringCheck(util::ConstevalFormatString<num_params> str) : fmt{str.fmt} {}
    30     operator const char*() { return fmt; }
    31     const char* fmt;
    

    maflcko commented at 10:07 am on December 11, 2024:

    I’m still not sure I see any downsides to adding LIFETIMEBOUND.

    Apart from the downsides I mentioned, LIFETIMEBOUND also isn’t supported on gcc. So using vanilla C++17 (or 20) seems more portable to issue compiler errors at compile time, but I don’t think it matters much in this case anyway.


    hodlinator commented at 11:05 am on December 11, 2024:

    LIFETIMEBOUND only being supported by one compiler isn’t a great reason to skip adding it. Good to let Clang do what it can, and also provide documentation to devs. Not blocking for me though.

    Tried the following, but Clang 18 compiles without any warnings when adding LIFETIMEBOUND.

    0std::optional<tinyformat::FormatStringCheck<0>> fsc;
    1{
    2    auto fmt{std::make_unique<tinyformat::RuntimeFormat>("{}")};
    3    fsc = {tinyformat::FormatStringCheck<0>(*fmt)};
    4}
    

    Also tried removing the RuntimeFormat-ctor from FormatStringCheck and found that it’s only being used in tinyformat::format(util::BilingualFmt... outside off fuzz-tests. So this should be a minor concern.


    maflcko commented at 11:47 am on December 11, 2024:

    LIFETIMEBOUND only being supported by one compiler isn’t a great reason to skip adding it.

    It is just one reason, among many that have been mentioned previously.

    Tried the following, but Clang 18 compiles without any warnings when adding LIFETIMEBOUND.

    Yes, I mentioned that the attribute right now doesn’t really do any useful checks at compile time in this context. (It is more useful in other contexts). This is one of the reasons why I preferred to use an approach that works at compile time on all compilers, only using the standard language.

    Though, I don’t think it matters much in this case anyway, so anything is fine here.


    ryanofsky commented at 3:12 pm on December 11, 2024:

    re: #31061 (review)

    Apart from the downsides I mentioned,

    It is just one reason, among many that have been mentioned previously.

    Thanks for adding LIFETIMEBOUND here. I’ve been pretty confused by this discussion because I haven’t seen any reasons given for why it would actually be better not to use LIFETIMEBOUND, and I don’t know of any reasons. I understand LIFETIMEBOUND doesn’t work everywhere, but don’t understand why that matters as long as it catches bugs and provides fast feedback in the places it does work, and is helpful everywhere to document references that need to be long-lived.

    If LIFETIMEBOUND triggered false positive errors like clang thread safety annotations do, that could be a real downside and reason not to use it. But as far as I know it doesn’t cause harms or have any negative effects like that, so I’ve been confused by the pushback here. Maybe the reasons not to use it are as simple as “looks ugly” / “adds noise” / “seems useless” which are perfectly ok (and are also reasons I don’t use const various places). i’d be fine with keeping or dropping LIFETIMEBOUND in this PR, just wondering if I am not understanding real problems with it.


    maflcko commented at 4:19 pm on December 11, 2024:
    I just think that compile failures are better than runtime errors or UB. Using vanilla C++17 code without attributes to turn bad code (such as https://godbolt.org/z/6336nGKcM (replace 0 with 1 to enable the compile-time checks)) into a compile failure on all compilers seems better than a runtime error, possibly only in CI.

    ryanofsky commented at 4:53 pm on December 11, 2024:

    Oh thanks for explaining that. I thought you were only disabling copies and assignment to prevent expensive copies due to not passing RuntimeFormat by reference, and that you were not passing RuntimeFormat by reference to avoid needing to think about lifetime issues.

    But actually your goal is to make RuntimeFormat run("fmt"); strprintf(run); a compiler error, and deleting the copy and assignment operators are a way to do that. So LIFETIMEBOUND isn’t harmful, but just not as good as a potentially safer solution fad431e548a694600a3fe212127052e5f922a622 that doesn’t use LIFETIMEBOUND.

    I think I can see now how fad431e548a694600a3fe212127052e5f922a622 actually is a safer solution in every case, and relying on mandatory copy elision is ok there because deleting the RuntimeFormat copy constructor should make it impossible to pass as a function argument without copy elision. This is more complicated than just using references, but does seem safer.


    hodlinator commented at 8:47 pm on December 11, 2024:

    https://godbolt.org/z/6336nGKcM (replace 0 with 1 to enable the compile-time checks)) into a compile failure on all compilers seems better than a runtime error, possibly only in CI.

    With the #if set to 1 and lines 42-43 changed to

    0        check = RuntimeFormat("fmt");
    

    One still gets an ASAN error instead of a compile time error.


    maflcko commented at 9:05 pm on December 11, 2024:

    I think it will always be possible to trick a C++ compiler into UB without a warning, possibly even all currently released compilers.

    If you wanted to solve the same problem for FormatStringCheck, you could apply the same approach (explicit+delete+copy-elision). However the patch would be too verbose for this codebase to be worth it.

    Also, the approach can only be applied in this context. And if you want to solve lifetime issues wholesale, it will need to happen in the language itself. Personally I liked the safe % references, which would allow new C++ code to be safe. However, I don’t think the proposal https://safecpp.org/draft-lifetimes.html will be in C++29, and possibly not make it at all.

  92. in src/tinyformat.h:185 in fab3893fa9 outdated
    178@@ -179,13 +179,19 @@ namespace tfm = tinyformat;
    179 
    180 namespace tinyformat {
    181 
    182+// Similar to std::runtime_format from C++26.
    183+struct RuntimeFormat {
    184+    explicit RuntimeFormat(std::string str) : fmt{std::move(str)} {}
    185+    std::string fmt;
    


    hodlinator commented at 1:23 pm on December 7, 2024:
    Noticed that the std-version uses string_view. Maybe add a note about why you didn’t go with that in the commit message, or consider switching.

    maflcko commented at 10:23 am on December 9, 2024:
    thx, added comment
  93. hodlinator commented at 1:30 pm on December 7, 2024: contributor

    Thanks for incorporating my previous feedback!

    Leaving some brief comments on the new changes before getting back to the weekend.

    Was lint-format-strings.py picked up by some glob-expression in CI?

  94. ryanofsky commented at 2:32 pm on December 7, 2024: contributor

    This looks pretty good but I think the API can be improved. Specifically I think .translate() suffixes should not be necessary and just calling _() should be sufficient to indicate that you want a translated std::string or bilingual_str. If you don’t want the translation, there should be no reason to call _()

    A suggested implementation dropping .translate() suffixes is: branch, diff

    • daffb340d1d7b2c5a5248b3189a920cd3709e2ae refactor: Delay translation of _() literals
    • 12e2e2bd21b3ff90df2afe91625ffcf930b0f48f refactor: Check translatable format strings at compile-time
    • 3da69f27d1a0ca3c675f3cb491943beae9dc4922 refactor: Introduce struct to hold a runtime format string
    • c357f990b71a7bd8b35abe08bd4ad309411a0325 lint: Remove unused and broken format string linter

    This also adds a little more test coverage so the original and translated format strings in the test are not the same and test can confirm the right strings are picked.

  95. in src/util/translation.h:82 in fab3893fa9 outdated
    74+} // namespace util
    75+
    76+consteval auto _(util::Translatable str) { return str; }
    77+
    78 /** Mark a bilingual_str as untranslated */
    79 inline bilingual_str Untranslated(std::string original) { return {original, original}; }
    


    l0rinc commented at 5:17 pm on December 8, 2024:

    slightly unrelated: can we use a const reference here instead?

    0inline bilingual_str Untranslated(const std::string& original) { return {original, original}; }
    

    maflcko commented at 10:23 am on December 9, 2024:
    Yes, it is possible, but adding const prevents a move, if someone wanted to write return {original, std::move(original)};

    hodlinator commented at 7:48 pm on December 10, 2024:

    Undefined parameter evaluation order would make it unclear which of them should be explicitly moved though? https://stackoverflow.com/a/2934909

    I hope the compiler introduces an implicit move for whichever parameter it deems to be the last use of the variable.


    maflcko commented at 8:07 pm on December 10, 2024:

    Undefined parameter evaluation order would make it unclear which of them should be explicitly moved though?

    Starting with C++17, return {original, std::move(original)}; is both legal and correct. See https://timsong-cpp.github.io/cppwp/n4868/dcl.init.general#18

    However, I’d leave this as-is for now, as the performance shouldn’t matter here, even if the compiler can’t optimize it out.

  96. maflcko force-pushed on Dec 9, 2024
  97. maflcko commented at 10:14 am on December 9, 2024: member

    Was lint-format-strings.py picked up by some glob-expression in CI?

    Yes, see:

    https://github.com/bitcoin/bitcoin/blob/35000e34cf339e46d62b757c3723057724d23637/test/lint/test_runner/src/main.rs#L557

    In the future it could make sense to explicitly list them one-by-one in the list, instead of using the implicit glob.

  98. ryanofsky approved
  99. ryanofsky commented at 10:32 pm on December 9, 2024: contributor
    Code review ACK fa7cb525b7069ce0197d2f309102432273fea4d6. LGTM!
  100. DrahtBot requested review from hodlinator on Dec 9, 2024
  101. DrahtBot requested review from l0rinc on Dec 9, 2024
  102. maflcko force-pushed on Dec 10, 2024
  103. maflcko commented at 11:01 am on December 10, 2024: member
    Looks like there is a codegen bug in msvc, or some other issue? It would be good if someone with a Windows box could take a look, to either debug the crash or create a minimal reproducer.
  104. maflcko marked this as a draft on Dec 10, 2024
  105. ryanofsky commented at 1:30 pm on December 10, 2024: contributor

    Looks like there is a codegen bug in msvc, or some other issue? It would be good if someone with a Windows box could take a look, to either debug the crash or create a minimal reproducer.

    I think maybe the previous fa37083b37e6 FormatStringCheck(const RuntimeFormat& run) was safe and current fad431e548a6 FormatStringCheck(RuntimeFormat run) is buggy and the msvc error is a real bug.

    Previously it was safe to call run.fmt.c_str() because run object was temporary but had a lifetime was as long as the full-expression. Now it seems run object is a copy of original RuntimeFormat object and only has a lifetime as long as the FormatStringCheck constructor.

    Not sure why this wouldn’t cause errors on other platforms. One thing that may be unique about the timeoffsets test case that is failing is that the format string is very long so maybe this is affected by a short string optimization.

  106. maflcko commented at 1:41 pm on December 10, 2024: member

    Not sure why this wouldn’t cause errors on other platforms. One thing that may be unique about the timeoffsets test case that is failing is that the format string is very long so maybe this is affected by a short string optimization.

    That seems unlikely, for several reasons:

    • The format string is checked at compile time via TranslatedLiteral -> BilingualFmt -> ConstevalFormatString -> FormatStringCheck (only the last step is done at runtime). However, no step involves a runtime format string, or a std::string constructor.
    • A std::unique_ptr<std::string> should sidestep the short-string optimization, putting even short strings into newly allocated memory. Thus, the problem should be visible on other platforms, or consistently on Windows.
  107. ryanofsky commented at 2:05 pm on December 10, 2024: contributor
    • However, no step involves a runtime format string, or a std::string constructor.

    Doesn’t the fact that it is translated mean that is uses a runtime format string and std::string? The original string will be a const char* but the string returned from the translation function will be a runtime std::string

  108. maflcko commented at 3:32 pm on December 10, 2024: member

    Ok, fair enough. Though, I still don’t see how the RuntimeFormat string is destructed before the FormatStringCheck, given that:

    • The only constructor of RuntimeFormat is marked explicit, meaning that it has to be constructed before being passed to FormatStringCheck
    • Any copies or moves are deleted, so the string can only be reset in memory on destruction, which happens after the FormatStringCheck destruction.
    • The std::unique_ptr<std::string> test should disable short-string optimizations and trigger the issue reliably, if it existed

    See also the godbolt: https://godbolt.org/z/M3fEnd8Wq

    Maybe I am missing something obvious, but if there is no bug in msvc, it would be good to know the exact bug in this code.

  109. ryanofsky commented at 4:11 pm on December 10, 2024: contributor

    Maybe I am missing something obvious, but if there is no bug in msvc, it would be good to know the exact bug in this code.

    I think the following line from fad431e548a694600a3fe212127052e5f922a622 is buggy because it is saving the result of c_str() call from a string that about to be destroyed:

    0FormatStringCheck(RuntimeFormat run) : fmt{run.fmt.c_str()} {}
    

    The run.fmt string object will be destroyed when the FormatStringCheck constructor finishes because run is a non-reference parameter, so it’s a effectively a local variable within FormatStringCheck method.

  110. ryanofsky commented at 4:35 pm on December 10, 2024: contributor

    I think the following line from https://github.com/bitcoin/bitcoin/commit/fad431e548a694600a3fe212127052e5f922a622 is buggy because it is saving the result of c_str() call from a string that about to be destroyed:

    Looking at this more I think the constructor is not buggy in c++17 as long as a temporary object is being passed as the constructor argument, because the mandatory copy elision described at the top of https://en.cppreference.com/w/cpp/language/copy_elision should apply, so the temporary should not be copied, and the temporary should have the lifetime of the full-expression. So maybe MSVC is at fault here, either for not applying mandatory copy-elision, or for not extending the lifetime of the constructor argument to outlive the constructor when it does apply copy-elision.

    I do think the FormatStringCheck constructor in fad431e548a694600a3fe212127052e5f922a622 is inherently fragile though, because it will be copying the constructor argument if a non-rvalue is passed, and calling .c_str() on the temporary copy that is about to be destroyed. It seems better to just use references and LIFETIMEBOUND as originally suggested and avoid temporaries and copies.

  111. maflcko force-pushed on Dec 10, 2024
  112. maflcko marked this as ready for review on Dec 10, 2024
  113. hodlinator commented at 10:52 pm on December 10, 2024: contributor

    I’d prefer it if every TranslatedLiteral instance didn’t have a function pointer riding on the side, but I do like the added testability.

    One could make the test-implementation of G_TRANSLATION_FUN support hooking it for specific tests. It still comes with some added overhead when it comes to benchmarks and fuzzing over current master though.

     0diff --git a/src/test/translation_tests.cpp b/src/test/translation_tests.cpp
     1index 2ebde95259..bcbfe9db34 100644
     2--- a/src/test/translation_tests.cpp
     3+++ b/src/test/translation_tests.cpp
     4@@ -2,6 +2,7 @@
     5 // Distributed under the MIT software license, see the accompanying
     6 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
     7 
     8+#include <test/util/setup_common.h>
     9 #include <tinyformat.h>
    10 #include <util/translation.h>
    11 
    12@@ -9,21 +10,24 @@
    13 
    14 BOOST_AUTO_TEST_SUITE(translation_tests)
    15 
    16-static TranslateFn translate{[](const char * str) {  return strprintf("t(%s)", str);  }};
    17-
    18 BOOST_AUTO_TEST_CASE(translation_namedparams)
    19 {
    20-    constexpr util::TranslatedLiteral format{"original [%s]", &translate};
    21+    assert(!G_TRANSLATION_FUN_HOOK);
    22+    G_TRANSLATION_FUN_HOOK = [](const char * str) { return strprintf("t(%s)", str); };
    23+
    24+    constexpr util::TranslatedLiteral format{"original [%s]"};
    25 
    26     bilingual_str arg{"original", "translated"};
    27     bilingual_str result{strprintf(format, arg)};
    28     BOOST_CHECK_EQUAL(result.original, "original [original]");
    29     BOOST_CHECK_EQUAL(result.translated, "t(original [translated])");
    30 
    31-    util::TranslatedLiteral arg2{"original", &translate};
    32+    util::TranslatedLiteral arg2{"original"};
    33     bilingual_str result2{strprintf(format, arg2)};
    34     BOOST_CHECK_EQUAL(result2.original, "original [original]");
    35     BOOST_CHECK_EQUAL(result2.translated, "t(original [t(original)])");
    36+
    37+    G_TRANSLATION_FUN_HOOK = {};
    38 }
    39 
    40 BOOST_AUTO_TEST_SUITE_END()
    41diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp
    42index 12feba09a5..947f1e1ca7 100644
    43--- a/src/test/util/setup_common.cpp
    44+++ b/src/test/util/setup_common.cpp
    45@@ -72,7 +72,11 @@ using node::LoadChainstate;
    46 using node::RegenerateCommitments;
    47 using node::VerifyLoadedChainstate;
    48 
    49-const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
    50+std::function<std::string(const char*)> G_TRANSLATION_FUN_HOOK;
    51+
    52+const std::function<std::string(const char*)> G_TRANSLATION_FUN{[] (const char* str) {
    53+    return G_TRANSLATION_FUN_HOOK ? G_TRANSLATION_FUN_HOOK(str) : str;
    54+}};
    55 
    56 constexpr inline auto TEST_DIR_PATH_ELEMENT{"test_common bitcoin"}; // Includes a space to catch possible path escape issues.
    57 /** Random context to get unique temp data dirs. Separate from m_rng, which can be seeded from a const env var */
    58diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h
    59index b0f7bdade2..f033f20dc8 100644
    60--- a/src/test/util/setup_common.h
    61+++ b/src/test/util/setup_common.h
    62@@ -43,6 +43,8 @@ extern const std::function<std::vector<const char*>()> G_TEST_COMMAND_LINE_ARGUM
    63 /** Retrieve the unit test name. */
    64 extern const std::function<std::string()> G_TEST_GET_FULL_NAME;
    65 
    66+extern std::function<std::string(const char*)> G_TRANSLATION_FUN_HOOK;
    67+
    68 static constexpr CAmount CENT{1000000};
    69 
    70 /** Register common test args. Shared across binaries that rely on the test framework. */
    71diff --git a/src/util/translation.h b/src/util/translation.h
    72index 3a9316479d..d618b22ee2 100644
    73--- a/src/util/translation.h
    74+++ b/src/util/translation.h
    75@@ -13,8 +13,7 @@
    76 #include <string>
    77 
    78 /** Translate a message to the native language of the user. */
    79-using TranslateFn = std::function<std::string(const char*)>;
    80-const extern TranslateFn G_TRANSLATION_FUN;
    81+const extern std::function<std::string(const char*)> G_TRANSLATION_FUN;
    82 
    83 /**
    84  * Bilingual messages:
    85@@ -54,10 +53,9 @@ namespace util {
    86 //! Compile-time literal string that can be translated with an optional translation function.
    87 struct TranslatedLiteral {
    88     const char* const original;
    89-    const TranslateFn* translate_fn;
    90 
    91-    consteval TranslatedLiteral(const char* str, const TranslateFn* fn = &G_TRANSLATION_FUN) : original{str}, translate_fn{fn} { assert(original); }
    92-    operator std::string() const { return translate_fn && *translate_fn ? (*translate_fn)(original) : original; }
    93+    consteval TranslatedLiteral(const char* str) : original{str} { assert(original); }
    94+    operator std::string() const { return G_TRANSLATION_FUN ? G_TRANSLATION_FUN(original) : original; }
    95     operator bilingual_str() const { return {original, std::string{*this}}; }
    96 };
    
  114. in src/test/translation_tests.cpp:16 in fa7cb525b7 outdated
     8@@ -9,13 +9,21 @@
     9 
    10 BOOST_AUTO_TEST_SUITE(translation_tests)
    11 
    12+static TranslateFn translate{[](const char * str) {  return strprintf("t(%s)", str);  }};
    13+
    14 BOOST_AUTO_TEST_CASE(translation_namedparams)
    15 {
    16+    constexpr util::TranslatedLiteral format{"original [%s]", &translate};
    


    ryanofsky commented at 2:35 am on December 11, 2024:

    In commit “refactor: Check translatable format strings at compile-time” (fa948257f332772f058cabffe0e313a0518da724)

    Could consider making the test a little more realistic with:

     0--- a/src/test/translation_tests.cpp
     1+++ b/src/test/translation_tests.cpp
     2@@ -11,17 +11,22 @@ BOOST_AUTO_TEST_SUITE(translation_tests)
     3 
     4 static TranslateFn translate{[](const char * str) {  return strprintf("t(%s)", str);  }};
     5 
     6+// Custom translation function _t(), similar to _() but internal to this test.
     7+static inline consteval auto _t(util::TranslatedLiteral str)
     8+{
     9+    str.translate_fn = &translate;
    10+    return str;
    11+}
    12+
    13 BOOST_AUTO_TEST_CASE(translation_namedparams)
    14 {
    15-    constexpr util::TranslatedLiteral format{"original [%s]", &translate};
    16-
    17     bilingual_str arg{"original", "translated"};
    18-    bilingual_str result{strprintf(format, arg)};
    19+    bilingual_str result{strprintf(_t("original [%s]"), arg)};
    20     BOOST_CHECK_EQUAL(result.original, "original [original]");
    21     BOOST_CHECK_EQUAL(result.translated, "t(original [translated])");
    22 
    23     util::TranslatedLiteral arg2{"original", &translate};
    24-    bilingual_str result2{strprintf(format, arg2)};
    25+    bilingual_str result2{strprintf(_t("original [%s]"), arg2)};
    26     BOOST_CHECK_EQUAL(result2.original, "original [original]");
    27     BOOST_CHECK_EQUAL(result2.translated, "t(original [t(original)])");
    28 }
    
  115. ryanofsky approved
  116. ryanofsky commented at 2:55 am on December 11, 2024: contributor

    Code review ACK fa7cb525b7069ce0197d2f309102432273fea4d6. Looks good but did leave some suggestions which could be saved for a follow-up.


    re: #31061 (comment)

    I’d prefer it if every TranslatedLiteral instance didn’t have a function pointer riding on the side, but I do like the added testability.

    I don’t think the TranslatedLiteral::translate_fn members should add overhead because TranslatedLiteral constructor is consteval, and in any case TranslatedLiteral objects are ephemeral, they exist only to call a translation function one time and then disappear. (The additional pointers also technically aren’t function pointers, but pointers to a std::function object.)

    The goal of the pointers isn’t just improve testability but also to provide extensibility, so for example a libbitcoinkernel application could use define its own _ translation function and seamlessly use strprintf with its own translated strings. Having the pointers also avoids the need to mutate shared global state during tests so, for example, unlike with the suggested change, other tests could run in parallel with the translation test without being affected, and if translation test throws, it will not leave G_TRANSLATION_FUN pointing at the wrong function.

  117. bitcoin deleted a comment on Dec 11, 2024
  118. DrahtBot added the label Needs rebase on Dec 11, 2024
  119. hodlinator commented at 9:30 am on December 11, 2024: contributor

    I don’t think the TranslatedLiteral::translate_fn members should add overhead because TranslatedLiteral constructor is consteval

    Good point, I glossed over TranslatedLiteral being consteval.

    Having the pointers also avoids the need to mutate shared global state during tests

    Current CTest executes tests in parallel through starting separate processes, but it might change at some point and keeping it less brittle is good.

  120. in src/util/translation.h:9 in fa7cb525b7 outdated
    5@@ -6,12 +6,15 @@
    6 #define BITCOIN_UTIL_TRANSLATION_H
    7 
    8 #include <tinyformat.h>
    9+#include <util/string.h>
    


    hodlinator commented at 9:50 am on December 11, 2024:

    nit: Arguably tinyformat is more external than util/string.h, but our copy lives with our code so it’s certainly not clear-cut.

    0#include <util/string.h>
    1
    2#include <tinyformat.h>
    
  121. maflcko force-pushed on Dec 11, 2024
  122. hodlinator approved
  123. hodlinator commented at 10:14 am on December 11, 2024: contributor

    re-ACK fa7cb525b7069ce0197d2f309102432273fea4d6

    git range-diff master fa6b1d2 fa7cb52

    Really like how _() is sufficient without .translated/.translate() thanks to TranslatedLiterals operator std::string.


    Checked out fa37083b37e613f0987fe74e0397d9ed90589e4b.

    Ran linter being removed in following commit (fa7cb525b7069ce0197d2f309102432273fea4d6):

    0₿ test/lint/lint-format-strings.py 
    1src/test/fuzz/util/wallet.h: Expected 0 argument(s) after format string but found 2 argument(s): strprintf(tfm::RuntimeFormat{desc_fmt}, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})
    2src/test/translation_tests.cpp: Expected 0 argument(s) after format string but found 1 argument(s): strprintf(format, arg2)
    

    Might want to do something about that if we want all commits to pass, not blocking for me though.

  124. maflcko force-pushed on Dec 11, 2024
  125. DrahtBot added the label CI failed on Dec 11, 2024
  126. DrahtBot commented at 10:29 am on December 11, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/34246270269

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  127. maflcko force-pushed on Dec 11, 2024
  128. hodlinator commented at 11:08 am on December 11, 2024: contributor

    nit: Noticed a typo in the linter removal commit message:

    “The linter has many >implmentation< bugs and missing features.”

  129. DrahtBot removed the label Needs rebase on Dec 11, 2024
  130. DrahtBot removed the label CI failed on Dec 11, 2024
  131. in src/util/translation.h:98 in fa0b20a1fa outdated
     96+            return arg.original;
     97+        } else {
     98+            return arg;
     99+        }
    100+    }};
    101+    const auto translate_arg{[](const auto& arg) -> const auto& {
    


    ryanofsky commented at 2:34 pm on December 11, 2024:

    In commit “refactor: Check translatable format strings at compile-time” (fa0b20a1fa7b4e360b54ab06991d441e6d685aeb)

    Not important, but probably would have made sense to call this translated_arg instead of translate_arg since this is not actually translating the argument or calling the translation function, but just returning a reference to translated or translatable version of the argument.


    maflcko commented at 12:47 pm on December 16, 2024:
    thx, done
  132. ryanofsky approved
  133. ryanofsky commented at 3:33 pm on December 11, 2024: contributor
    Code review ACK fadd2e69c6176ba684c1fd06fbd765be6fb6d647. Just rebased and added lifetimebound and test suggestions since last review. Appreciate your patience with all the review comments and iterations of this PR. Will be really nice to see this merged with the new compiler checks being safer and more convenient than current runtime errors.
  134. DrahtBot requested review from hodlinator on Dec 11, 2024
  135. hodlinator approved
  136. hodlinator commented at 9:11 pm on December 11, 2024: contributor

    re-ACK fadd2e69c6176ba684c1fd06fbd765be6fb6d647

    git range-diff master fa7cb52 fadd2e6

    • Mysteriously grinding out fa-prefixed hashes.
    • Changed order of linter removal so that it doesn’t error on what is now the last commit.
    • Adapted unit tests further after feedback.
    • Reconciled silent merge conflict with #30933.
    • Added LIFETIMEBOUND to at least serve as developer documentation even though Clang isn’t able to fully use it.

    nit: Did not fix typo: #31061 (comment)

  137. refactor: Delay translation of _() literals
    This is required for a future commit that requires _() to be consteval
    for format literals.
    
    Co-Authored-By: Ryan Ofsky <ryan@ofsky.org>
    fad276ad65
  138. refactor: Check translatable format strings at compile-time fa93b05a4d
  139. lint: Remove unused and broken format string linter
    The linter has many implementation bugs and missing features.
    
    Also, it is completely redundant with FormatStringCheck, which
    constructs from ConstevalFormatString or a runtime format string.
    fafd72e6eb
  140. refactor: Introduce struct to hold a runtime format string
    This brings the format types closer to the standard library types:
    
    * FormatStringCheck corresponds to std::basic_format_string, with
      compile-time checks done via ConstevalFormatString
    * RuntimeFormat corresponds to std::runtime_format, with no compile-time
      checks done.
    
    Also, it documents where no compile-time checks are done.
    fa6d9a2916
  141. maflcko force-pushed on Dec 16, 2024
  142. hodlinator approved
  143. hodlinator commented at 2:17 pm on December 16, 2024: contributor

    re-ACK fa6d9a29162fb508385201c926ec745d071086fc

    git range-diff master fadd2e6 fa6d9a2

    • Fixed typo in commit message
    • Changed translate -> translated in lambda, ++correctness
  144. DrahtBot requested review from ryanofsky on Dec 16, 2024
  145. ryanofsky approved
  146. ryanofsky commented at 4:31 pm on December 17, 2024: contributor
    Code review ACK fa6d9a29162fb508385201c926ec745d071086fc. Just rebase, spelling fix and suggested rename since last review.

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2024-12-26 15:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me