Skip to content

[clang-format] Add BreakBinaryOperations configuration #95013

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Aug 10, 2024

Conversation

ameerj
Copy link
Contributor

@ameerj ameerj commented Jun 10, 2024

By default, clang-format packs binary operations, but it may be desirable to have compound operations be on individual lines instead of being packed.

This PR adds the option BreakBinaryOperations to break up large compound binary operations to be on one line each.

This applies to all logical and arithmetic/bitwise binary operations

Maybe partially addresses #79487 ?
Closes #58014
Closes #57280

@llvmbot llvmbot added clang Clang issues not falling into any other category clang-format labels Jun 10, 2024
@llvmbot
Copy link
Member

llvmbot commented Jun 10, 2024

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-format

Author: Ameer J (ameerj)

Changes

By default, clang-format packs binary operations, but it may be desirable to have compound operations be on individual lines instead of being packed.

This PR adds the option BinPackBinaryOperations, which mimics the behavior of BinPackArguments and BinPackParameters, but applied to binary operations.
When BinPackBinaryOperations is set to false, binary operations will either be all on the same line, or each operation will have one line each.

This applies to all logical and arithmetic/bitwise binary operations

Maybe partially addresses #79487 ?


Full diff: https://github.com/llvm/llvm-project/pull/95013.diff

5 Files Affected:

  • (modified) clang/docs/ClangFormatStyleOptions.rst (+17)
  • (modified) clang/include/clang/Format/Format.h (+16)
  • (modified) clang/lib/Format/ContinuationIndenter.cpp (+15)
  • (modified) clang/lib/Format/Format.cpp (+2)
  • (modified) clang/unittests/Format/FormatTest.cpp (+80)
diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index bb00c20922d36..246d98eef63e8 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -2065,6 +2065,23 @@ the configuration (without a prefix: ``Auto``).
         aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa);
     }
 
+.. _BinPackBinaryOperations:
+
+**BinPackBinaryOperations** (``Boolean``) :versionbadge:`clang-format 19` :ref:`¶ <BinPackBinaryOperations>`
+  If ``false``, binary operations will either be all on the same line
+  or each operation will have one line each.
+
+  .. code-block:: c++
+
+    true:
+    aaaaaaaaaaaaaaaaaaaa && aaaaaaaaaaaaaaaaaaaa ||
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
+
+    false:
+    aaaaaaaaaaaaaaaaaaaa &&
+    aaaaaaaaaaaaaaaaaaaa ||
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
+
 .. _BinPackParameters:
 
 **BinPackParameters** (``Boolean``) :versionbadge:`clang-format 3.7` :ref:`¶ <BinPackParameters>`
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 4fd6e013df25b..19b9c60ab6e99 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -1192,6 +1192,21 @@ struct FormatStyle {
   /// \version 3.7
   bool BinPackArguments;
 
+  /// If ``false``, binary operations will either be all on the same line
+  /// or each operation will have one line each.
+  /// \code
+  ///   true:
+  ///   aaaaaaaaaaaaaaaaaaaa && aaaaaaaaaaaaaaaaaaaa ||
+  ///   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
+  ///
+  ///   false:
+  ///   aaaaaaaaaaaaaaaaaaaa &&
+  ///   aaaaaaaaaaaaaaaaaaaa ||
+  ///   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
+  /// \endcode
+  /// \version 19
+  bool BinPackBinaryOperations;
+
   /// If ``false``, a function declaration's or function definition's
   /// parameters will either all be on the same line or will have one line each.
   /// \code
@@ -4978,6 +4993,7 @@ struct FormatStyle {
                R.AlwaysBreakBeforeMultilineStrings &&
            AttributeMacros == R.AttributeMacros &&
            BinPackArguments == R.BinPackArguments &&
+           BinPackBinaryOperations == R.BinPackBinaryOperations &&
            BinPackParameters == R.BinPackParameters &&
            BitFieldColonSpacing == R.BitFieldColonSpacing &&
            BracedInitializerIndentWidth == R.BracedInitializerIndentWidth &&
diff --git a/clang/lib/Format/ContinuationIndenter.cpp b/clang/lib/Format/ContinuationIndenter.cpp
index b07360425ca6e..0780d219b68b4 100644
--- a/clang/lib/Format/ContinuationIndenter.cpp
+++ b/clang/lib/Format/ContinuationIndenter.cpp
@@ -146,6 +146,14 @@ static bool startsNextParameter(const FormatToken &Current,
            Style.BreakInheritanceList != FormatStyle::BILS_BeforeComma));
 }
 
+// Returns \c true if \c Current starts a new operand in a binary operation.
+static bool startsNextOperand(const FormatToken &Current,
+                              const FormatStyle &Style) {
+  const FormatToken &Previous = *Current.Previous;
+  return Previous.is(TT_BinaryOperator) && !Current.isTrailingComment() &&
+         (Previous.getPrecedence() > prec::Conditional);
+}
+
 static bool opensProtoMessageField(const FormatToken &LessTok,
                                    const FormatStyle &Style) {
   if (LessTok.isNot(tok::less))
@@ -837,6 +845,9 @@ void ContinuationIndenter::addTokenOnCurrentLine(LineState &State, bool DryRun,
   }
   if (CurrentState.AvoidBinPacking && startsNextParameter(Current, Style))
     CurrentState.NoLineBreak = true;
+  if (!Style.BinPackBinaryOperations && startsNextOperand(Current, Style))
+    CurrentState.NoLineBreak = true;
+
   if (startsSegmentOfBuilderTypeCall(Current) &&
       State.Column > getNewLineColumn(State)) {
     CurrentState.ContainsUnwrappedBuilder = true;
@@ -1203,6 +1214,10 @@ unsigned ContinuationIndenter::addTokenOnNewLine(LineState &State,
     }
   }
 
+  if (!Style.BinPackBinaryOperations && Previous.is(TT_BinaryOperator) &&
+      (Previous.getPrecedence() > prec::Conditional)) {
+    CurrentState.BreakBeforeParameter = true;
+  }
   return Penalty;
 }
 
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index c015e03fa15e7..4d71ce95afe1d 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -935,6 +935,7 @@ template <> struct MappingTraits<FormatStyle> {
                    Style.AlwaysBreakBeforeMultilineStrings);
     IO.mapOptional("AttributeMacros", Style.AttributeMacros);
     IO.mapOptional("BinPackArguments", Style.BinPackArguments);
+    IO.mapOptional("BinPackBinaryOperations", Style.BinPackBinaryOperations);
     IO.mapOptional("BinPackParameters", Style.BinPackParameters);
     IO.mapOptional("BitFieldColonSpacing", Style.BitFieldColonSpacing);
     IO.mapOptional("BracedInitializerIndentWidth",
@@ -1439,6 +1440,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
   LLVMStyle.AlwaysBreakBeforeMultilineStrings = false;
   LLVMStyle.AttributeMacros.push_back("__capability");
   LLVMStyle.BinPackArguments = true;
+  LLVMStyle.BinPackBinaryOperations = true;
   LLVMStyle.BinPackParameters = true;
   LLVMStyle.BitFieldColonSpacing = FormatStyle::BFCS_Both;
   LLVMStyle.BracedInitializerIndentWidth = std::nullopt;
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index fb57333858529..cb20c5db4884b 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -27492,6 +27492,86 @@ TEST_F(FormatTest, SpaceBetweenKeywordAndLiteral) {
   verifyFormat("return sizeof \"5\";");
 }
 
+TEST_F(FormatTest, BinPackBinaryOperations) {
+  auto Style = getLLVMStyle();
+  Style.BinPackBinaryOperations = false;
+
+  // Logical operations
+  verifyFormat("if (condition1 && condition2) {\n"
+               "}",
+               Style);
+  verifyFormat("if (condition1 && // comment\n"
+               "    condition2 &&\n"
+               "    (condition3 || condition4) && // comment\n"
+               "    condition5 &&\n"
+               "    condition6) {\n"
+               "}",
+               Style);
+  verifyFormat("if (loooooooooooooooooooooongcondition1 &&\n"
+               "    loooooooooooooooooooooongcondition2) {\n"
+               "}",
+               Style);
+
+  // Arithmetic
+  verifyFormat("const int result = lhs + rhs;\n", Style);
+  verifyFormat("const int result = loooooooooooooooooooooongop1 +\n"
+               "                   loooooooooooooooooooooongop2 +\n"
+               "                   loooooooooooooooooooooongop3;\n",
+               Style);
+
+  // clang-format off
+  verifyFormat("const int result =\n"
+               "    operand1 + operand2 - (operand3 + operand4) - operand5 * operand6;\n",
+               Style);
+  // clang-format on
+
+  verifyFormat("const int result = longOperand1 +\n"
+               "                   longOperand2 -\n"
+               "                   (longOperand3 + longOperand4) -\n"
+               "                   longOperand5 * longOperand6;\n",
+               Style);
+
+  Style.BinPackBinaryOperations = true;
+
+  // Logical operations
+  verifyFormat("if (condition1 && condition2) {\n"
+               "}",
+               Style);
+
+  // clang-format off
+  verifyFormat("if (condition1 && condition2 && (condition3 || condition4) && condition5 &&\n"
+               "    condition6) {\n"
+               "}",
+               Style);
+  // clang-format on
+
+  verifyFormat("if (loooooooooooooooooooooongcondition1 &&\n"
+               "    loooooooooooooooooooooongcondition2) {\n"
+               "}",
+               Style);
+
+  // Arithmetic
+  verifyFormat("const int result = lhs + rhs;\n", Style);
+
+  // clang-format off
+  verifyFormat("const int result = loooooooooooooooooooooongop1 + loooooooooooooooooooooongop2 +\n"
+               "                   loooooooooooooooooooooongop3;\n",
+               Style);
+  // clang-format on
+
+  // clang-format off
+  verifyFormat("const int result = longOperand1 + longOperand2 - (longOperand3 + longOperand4) -\n"
+               "                   longOperand5 * longOperand6;\n",
+               Style);
+  // clang-format on
+
+  // clang-format off
+  verifyFormat("const int result =\n"
+               "    operand1 + operand2 - (operand3 + operand4) - operand5 * operand6;\n",
+               Style);
+  // clang-format on
+}
+
 } // namespace
 } // namespace test
 } // namespace format

Copy link

github-actions bot commented Jul 1, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

Copy link
Contributor

@HazardyKnusperkeks HazardyKnusperkeks left a comment

Choose a reason for hiding this comment

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

Could you please add a note to clang/docs/ReleaseNotes.rst.

@ameerj
Copy link
Contributor Author

ameerj commented Jul 9, 2024

Thanks for the review and approval @HazardyKnusperkeks !

I don't have write permissions to the repo so please merge this for me when you have a moment.

Copy link
Contributor

@owenca owenca left a comment

Choose a reason for hiding this comment

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

Also, we need ConfigParseTest for the new option.

@owenca
Copy link
Contributor

owenca commented Jul 16, 2024

This patch doesn't seem to work with the last two examples in #79487:

$ cat foo.cc
template<typename T>
concept my_concept =
       std::is_object_v<T>
    && std::is_const_v<T>
    && std::default_initializable<T>;

std::uint32_t a =
      byte_buffer[0]
    | byte_buffer[1] << 8
    | byte_buffer[2] << 16
    | byte_buffer[3] << 24;
$ clang-format -style='{BinPackBinaryOperations: false, BreakBeforeBinaryOperators: NonAssignment, ColumnLimit: 50}' foo.cc
template <typename T>
concept my_concept =
    std::is_object_v<T>
    && std::is_const_v<T> && std::default_initializable<T>;

std::uint32_t a =
    byte_buffer[0]
    | byte_buffer[1] << 8 | byte_buffer[2] << 16 | byte_buffer[3] << 24;
$ 

The logical && and bitwise | operations are not broken up into one per line, and the column limit is exceeded.

@owenca
Copy link
Contributor

owenca commented Jul 16, 2024

It seems that this patch implements the new option without regard to the relative precedence of the binary operators:

$ cat bar.cc
foooooooooooo1 = foooooooooooo2 * foooooooooooo3 +
                 foooooooooooo4 / foooooooooooo5 - foooooooooooo6;
$ clang-format -style='{BinPackBinaryOperations: false}' bar.cc
foooooooooooo1 = foooooooooooo2 *
                 foooooooooooo3 +
                 foooooooooooo4 /
                 foooooooooooo5 -
                 foooooooooooo6;
$ 

@mydeveloperday @HazardyKnusperkeks @rymiel are you okay with this?

@HazardyKnusperkeks
Copy link
Contributor

It seems that this patch implements the new option without regard to the relative precedence of the binary operators:

$ cat bar.cc
foooooooooooo1 = foooooooooooo2 * foooooooooooo3 +
                 foooooooooooo4 / foooooooooooo5 - foooooooooooo6;
$ clang-format -style='{BinPackBinaryOperations: false}' bar.cc
foooooooooooo1 = foooooooooooo2 *
                 foooooooooooo3 +
                 foooooooooooo4 /
                 foooooooooooo5 -
                 foooooooooooo6;
$ 

@mydeveloperday @HazardyKnusperkeks @rymiel are you okay with this?

Per documentation that is exactly intended. So I'm in favor of that behavior.

@mydeveloperday
Copy link
Contributor

I'm not finding the true/false every clear, can we use an enum?

@owenca
Copy link
Contributor

owenca commented Jul 17, 2024

It seems AlignOperands is supposed to do what this new option would, so maybe we should fix/extend AlignOperands instead?

@ameerj
Copy link
Contributor Author

ameerj commented Jul 17, 2024

The logical && and bitwise | operations are not broken up into one per line, and the column limit is exceeded.

@owenca this should be fixed in the latest commit

@ameerj
Copy link
Contributor Author

ameerj commented Jul 17, 2024

It seems AlignOperands is supposed to do what this new option would, so maybe we should fix/extend AlignOperands instead?

This new setting is only responsible for breaking long expressions, not aligning.
AlignOperands can be used in conjunction with this new setting for different alignment styles.

@ameerj
Copy link
Contributor Author

ameerj commented Jul 17, 2024

I'm not finding the true/false every clear, can we use an enum?

Reworked this to now be an enum. Please let me know if the intent is more clear now or if anything needs further clarification.

@ameerj ameerj changed the title [clang-format] Add BinPackBinaryOperations configuration [clang-format] Add BreakBinaryOperations configuration Jul 17, 2024
Copy link
Contributor

@mydeveloperday mydeveloperday left a comment

Choose a reason for hiding this comment

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

I think the enum is clearer and leave you open to add new options as you did, thank you,

Copy link
Contributor

@mydeveloperday mydeveloperday left a comment

Choose a reason for hiding this comment

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

I have no major objection I'll go with whatever @owenca and @HazardyKnusperkeks agree

@HazardyKnusperkeks
Copy link
Contributor

Fine by me. The build states:

Failed Tests (1):
Clang-Unit :: Format/./FormatTests/ConfigParseTest/ParsesConfiguration

Is this from this change?

@ameerj
Copy link
Contributor Author

ameerj commented Jul 22, 2024

Failed Tests (1):

Fixed in the latest commit

@ameerj
Copy link
Contributor Author

ameerj commented Jul 29, 2024

@owenca Can you review once more please?

Copy link
Contributor

@owenca owenca left a comment

Choose a reason for hiding this comment

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

This patch also fixes #58014 and #57280, so you can include them in the commit message.

@owenca owenca removed the clang Clang issues not falling into any other category label Aug 5, 2024
@ameerj
Copy link
Contributor Author

ameerj commented Aug 9, 2024

Thanks for the review feedback @owenca @HazardyKnusperkeks
Please merge this PR for me if you feel it is ready as I don't have merge permissions

@owenca
Copy link
Contributor

owenca commented Aug 9, 2024

Thanks for the review feedback @owenca @HazardyKnusperkeks Please merge this PR for me if you feel it is ready as I don't have merge permissions

I wonder if anyone can come up with a better name than OnePerLine. See #101882 (comment). @mydeveloperday @HazardyKnusperkeks @rymiel? If not, I can merge this patch as is.

@HazardyKnusperkeks
Copy link
Contributor

Nah, I can't find a better name, which isn't a whole sentence.

@owenca owenca merged commit c5a4291 into llvm:main Aug 10, 2024
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[clang-format] AllowBreakingBinaryOperators needed clang-format: Consider one statement per line for chained bool equalities
5 participants