This is an archive of the discontinued LLVM Phabricator instance.

[clang][sema] Generate builtin operator overloads for (volatile) _Atomic types
ClosedPublic

Authored by jansvoboda11 on May 10 2022, 5:32 PM.

Details

Summary

We observed a failed assert in overloaded compound-assignment operator resolution:

Assertion failed: (Result.isInvalid() && "C++ binary operator overloading is missing candidates!"), function CreateOverloadedBinOp, file SemaOverload.cpp, line 13944.
...
frame #4: clang` clang::Sema::CreateOverloadedBinOp(..., Opc=BO_OrAssign, ..., PerformADL=true, AllowRewrittenCandidates=false, ...) at SemaOverload.cpp:13943
frame #5: clang` BuildOverloadedBinOp(..., Opc=BO_OrAssign, ...) at SemaExpr.cpp:15228
frame #6: clang` clang::Sema::BuildBinOp(..., Opc=BO_OrAssign, ...) at SemaExpr.cpp:15330
frame #7: clang` clang::Sema::ActOnBinOp(..., Kind=pipeequal, ...) at SemaExpr.cpp:15187
frame #8: clang` clang::Parser::ParseRHSOfBinaryExpression(..., MinPrec=Assignment) at ParseExpr.cpp:629
frame #9: clang` clang::Parser::ParseAssignmentExpression(..., isTypeCast=NotTypeCast) at ParseExpr.cpp:176
frame #10: clang` clang::Parser::ParseExpression(... isTypeCast=NotTypeCast) at ParseExpr.cpp:124
frame #11: clang` clang::Parser::ParseExprStatement(...) at ParseStmt.cpp:464

A simple reproducer is:

_Atomic unsigned an_atomic_uint;

enum { an_enum_value = 1 };

void enum1() { an_atomic_uint += an_enum_value; }

This patch fixes the issue by generating builtin operator overloads for (volatile) _Atomic types.

Diff Detail

Event Timeline

jkorous created this revision.May 10 2022, 5:32 PM
Herald added a project: Restricted Project. · View Herald TranscriptMay 10 2022, 5:32 PM
jkorous requested review of this revision.May 10 2022, 5:32 PM
jkorous added reviewers: rsmith, ahatanak, rjmccall.
jkorous edited the summary of this revision. (Show Details)May 10 2022, 5:47 PM

It's interesting to note that an_atomic_uint = an_atomic_uint + an_enum_value works correctly: https://godbolt.org/z/cvP9e6nh7. I was trying to figure out whether the atomic qualifier is properly stripped for the compound operator. When I run under a debugger and dump the AST for Args[0], I get: DeclRefExpr 0x26efcf87f88 '_Atomic(unsigned int)' lvalue Var 0x26efcf44cb8 'an_atomic_uint' '_Atomic(unsigned int)' which seems like it may be the root cause of the problem here (tough to say given that this is a C extension in C++ though). The lvalue conversion that takes place for an_atomic_uint should drop the atomic qualifier per C2x 6.3.2.1p2.

clang/lib/Sema/SemaOverload.cpp
13721

The behavior isn't specific to GNU mode: https://godbolt.org/z/7dezsTEas

Is it not possible to handle this similarly to volatile unsigned? If I replace _Atomic unsigned with volatile unsigned, I see LookupOverloadedBinOp succeed without having to strip volatile because addAssignmentArithmeticOverloads adds candidates with volatile types.

Is it not possible to handle this similarly to volatile unsigned? If I replace _Atomic unsigned with volatile unsigned, I see LookupOverloadedBinOp succeed without having to strip volatile because addAssignmentArithmeticOverloads adds candidates with volatile types.

Possibly? I am just generally apprehensive about changing the C++ side of things as I can't imagine all the consequences.
One difference that I see is the existence of volatile-qualified versions of these operators being prescribed by C++ standard while the operators you suggest definitely aren't:

// C++ [over.built]p18:
//
//   For every triple (L, VQ, R), where L is an arithmetic type,
//   VQ is either volatile or empty, and R is a promoted
//   arithmetic type, there exist candidate operator functions of
//   the form

https://github.com/llvm/llvm-project/blob/main/clang/lib/Sema/SemaOverload.cpp#L8926

It's interesting to note that an_atomic_uint = an_atomic_uint + an_enum_value works correctly: https://godbolt.org/z/cvP9e6nh7. I was trying to figure out whether the atomic qualifier is properly stripped for the compound operator. When I run under a debugger and dump the AST for Args[0], I get: DeclRefExpr 0x26efcf87f88 '_Atomic(unsigned int)' lvalue Var 0x26efcf44cb8 'an_atomic_uint' '_Atomic(unsigned int)' which seems like it may be the root cause of the problem here (tough to say given that this is a C extension in C++ though). The lvalue conversion that takes place for an_atomic_uint should drop the atomic qualifier per C2x 6.3.2.1p2.

Thank you for the suggestion! I'm looking into this.

jansvoboda11 commandeered this revision.Jun 8 2022, 4:07 AM
jansvoboda11 added a reviewer: jkorous.
jansvoboda11 added a subscriber: jansvoboda11.

Jan has asked me to take over.

Add operators for atomic types

Herald added a project: Restricted Project. · View Herald TranscriptJun 8 2022, 5:05 AM

I tried the approach suggested by @ahatanak. WDYT?

This seems like the right idea to me, yeah. It doesn't look like the patch handles volatile _Atomic correctly, though.

I know we do a lot of candidate prefiltering, and that that can be difficult because of uesr-defined conversions. How do those things interact with qualifiers? Like, I notice the existing code is adding both (T &, T) and (volatile T &, T) when the LHS is volatile; is there a reason that's necessary? Because we can't actually convert the LHS to remove those qualifiers, and adding combinatoric candidates could get very expensive if we start doing it for arbitrary extended qualifiers, which it feels like we ought to. (You could certainly have e.g. a volatile _Atomic __myaddrspace l-value.) If this is only necessary when the LHS has a user-defined conversion, then maybe we could at least not do it in the normal case just because the RHS is an overloaded type.

Handle volatile _Atomic as well

It doesn't look like the patch handles volatile _Atomic correctly, though.

That should be fixed in the new revision.

I know we do a lot of candidate prefiltering, and that that can be difficult because of uesr-defined conversions. How do those things interact with qualifiers?

I don't know.

Like, I notice the existing code is adding both (T &, T) and (volatile T &, T) when the LHS is volatile; is there a reason that's necessary?

Looking at Git history, that was always the case. I don't see any particular reason we do that eagerly.

Because we can't actually convert the LHS to remove those qualifiers, and adding combinatoric candidates could get very expensive if we start doing it for arbitrary extended qualifiers, which it feels like we ought to. (You could certainly have e.g. a volatile _Atomic __myaddrspace l-value.) If this is only necessary when the LHS has a user-defined conversion, then maybe we could at least not do it in the normal case just because the RHS is an overloaded type.

Yup, seems like we could be more lazy/minimal about that. I can look into that as a follow-up.

aaron.ballman added inline comments.Jun 9 2022, 7:06 AM
clang/lib/Sema/SemaOverload.cpp
9012

Do we need to handle atomic vector types?

rjmccall added inline comments.Jun 9 2022, 8:24 AM
clang/lib/Sema/SemaOverload.cpp
9002–9008

Can we do this in a way that's a little more general if we want to include other qualifiers in this combinatoric explosion in the future? I think you just need to make a recursive helper function that takes a set of qualifiers to break down (treating the atomic flag as a "qualifier" for this purpose) and then calls a callback for all combinations of those qualifiers. Like:

struct QualifiersAndAtomic {
  Qualifiers Quals;
  bool HasAtomic;
  QualifiersAndAtomic() : HasAtomic(false) {}
  QualifiersAndAtomic(Qualifiers quals, bool hasAtomic) : Quals(quals), HasAtomic(hasAtomic) {}

  QualifiersAndAtomic withVolatile() { return {Quals.withVolatile(), HasAtomic}; }
  QualifiersAndAtomic withAtomic() { return {Quals, true}; }
};

static void forAllQualifierCombinations(QualifiersAndAtomic available, QualifiersAndAtomic applied, llvm::function_ref<void(QualifiersAndAtomic)> callback) {
  // _Atomic
  if (available.HasAtomic) {
    available.HasAtomic = false;
    forAllQualifierCombinations(available, applied.withAtomic());
    forAllQualifierCombinations(available, applied);
    return;
  }

  // volatile
  if (available.Quals.hasVolatile()) {
    available.Quals.removeVolatile();
    assert(!applied.Quals.hasVolatile());
    forAllQualifierCombinations(available, applied.withVolatile());
    forAllQualifierCombinations(available, applied);
    return;
  }

  callback(applied);
}

The QualifiersAndAtomic structure is probably useful enough to go in Type.h.

Introduce QualifiersAndAtomic

jansvoboda11 marked an inline comment as done.Jun 9 2022, 2:20 PM
jansvoboda11 added inline comments.
clang/lib/Sema/SemaOverload.cpp
9012

I tried those, but it appears vector types don't accept _Atomic element types:

typedef _Atomic unsigned v_a_unsigned __attribute__((__vector_size__(16)));
// error: invalid vector element type '_Atomic(unsigned int)'

And _Atomic vector gives this error:

typedef unsigned v_unsigned __attribute__((__vector_size__(16)));
typedef _Atomic v_unsigned a_v_unsigned;

a_v_unsigned avu;

void enum5() { avu += an_enum_value; }
void enum6() { avu |= an_enum_value; }

I'm not an expert in this area, so I can't say for sure this is the correct behavior. But at least we don't crash.

jansvoboda11 added inline comments.Jun 10 2022, 12:20 AM
clang/lib/Sema/SemaOverload.cpp
9012

And _Atomic vector gives this error:

error: invalid operands to binary expression
rjmccall added inline comments.Jun 10 2022, 6:28 AM
clang/include/clang/AST/Type.h
282

Could you go ahead and add this for const and restrict as well? I think that will give us the full set.

clang/lib/Sema/SemaOverload.cpp
8248

Extremely minor style nit: standard style is to declare these static rather than in an anonymous namespace. And it's probably worthwhile to say in a comment that we're currently only handling the qualifiers that are meaningful for the LHS of compound assignment overloading.

9012

The way to get an answer here, I think, is to see if it works with something like a single scalar int.

Unfortunately, it probably does need to work (maybe conditionally), because at least some of our supported vector types have an implicit "splat" promotion from scalars, so you have the enum -> int -> splat conversion path. But we don't have to do that in this patch.

aaron.ballman added inline comments.Jun 10 2022, 8:00 AM
clang/include/clang/AST/Type.h
627

The set of operations here feel a bit weird. We have withVolatile and withAtomic but not withConst or withRestrict. We have hasVolatile and hasRestrict but no hasConst or hasAtomic. And we have addConst but no other add methods for the other qualifiers.

Should these sets be rounded out to cover all the qualifiers? I know we don't need them for the functionality you're fixing up, but this seems like a pretty generic interface in a header file that's used all over the place.

clang/lib/Sema/SemaOverload.cpp
9012

But we don't have to do that in this patch.

+1, if it's involved, it can definitely be done in a follow-up.

Code review feedback

Implement another round of review feedback

jansvoboda11 marked 2 inline comments as done.Jun 10 2022, 8:47 AM

Minor suggestion for a cleanup, then LGTM.

clang/lib/Sema/SemaOverload.cpp
9063

Can you add a helper function to build this qualified l-value reference type from a QualifiersAndAtomic?

aaron.ballman accepted this revision.Jun 13 2022, 11:44 AM

LGTM modulo the suggestion from @rjmccall, thank you for this!

This revision is now accepted and ready to land.Jun 13 2022, 11:44 AM
jansvoboda11 retitled this revision from [Sema] Fix crash for C11 atomic in gnu++ mode to [clang][sema] Generate builtin operator overloads for (volatile) _Atomic types.Jun 20 2022, 1:55 AM
jansvoboda11 edited the summary of this revision. (Show Details)

Extract function for building type

jansvoboda11 marked an inline comment as done.Jun 20 2022, 1:56 AM
This revision was landed with ongoing or failed builds.Jun 20 2022, 2:03 AM
This revision was automatically updated to reflect the committed changes.