Skip to content

[C23] Handle type compatibility for enumerations better #150282

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 4 commits into from
Jul 29, 2025

Conversation

AaronBallman
Copy link
Collaborator

An enumeration is compatible with its underlying type, which means that code like the following should be accepted:

struct A { int h; };
void func() {
extern struct A x;
enum E : int { e };
struct A { enum E h; };
extern struct A x;
}

because the structures are declared in different scopes, the two declarations of 'x' are both compatible.

Note, the structural equivalence checker does not take scope into account, but that is something the C standard requires. This means we are accepting code we should be rejecting per the standard, like:

void func() {
struct A { int h; };
extern struct A x;
enum E : int { e };
struct A { enum E h; };
extern struct A x;
}

Because the structures are declared in the same scope, the type compatibility rule require the structures to use the same types, not merely compatible ones.

Fixes #149965

An enumeration is compatible with its underlying type, which means that
code like the following should be accepted:

  struct A { int h; };
  void func() {
    extern struct A x;
    enum E : int { e };
    struct A { enum E h; };
    extern struct A x;
  }

because the structures are declared in different scopes, the two
declarations of 'x' are both compatible.

Note, the structural equivalence checker does not take scope into
account, but that is something the C standard requires. This means we
are accepting code we should be rejecting per the standard, like:

  void func() {
    struct A { int h; };
    extern struct A x;
    enum E : int { e };
    struct A { enum E h; };
    extern struct A x;
  }

Because the structures are declared in the same scope, the type
compatibility rule require the structures to use the same types, not
merely compatible ones.

Fixes llvm#149965
@AaronBallman AaronBallman added clang Clang issues not falling into any other category c23 clang:frontend Language frontend issues, e.g. anything involving "Sema" diverges-from:gcc Does the clang frontend diverge from gcc on this issue labels Jul 23, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 23, 2025

@llvm/pr-subscribers-clang

Author: Aaron Ballman (AaronBallman)

Changes

An enumeration is compatible with its underlying type, which means that code like the following should be accepted:

struct A { int h; };
void func() {
extern struct A x;
enum E : int { e };
struct A { enum E h; };
extern struct A x;
}

because the structures are declared in different scopes, the two declarations of 'x' are both compatible.

Note, the structural equivalence checker does not take scope into account, but that is something the C standard requires. This means we are accepting code we should be rejecting per the standard, like:

void func() {
struct A { int h; };
extern struct A x;
enum E : int { e };
struct A { enum E h; };
extern struct A x;
}

Because the structures are declared in the same scope, the type compatibility rule require the structures to use the same types, not merely compatible ones.

Fixes #149965


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

2 Files Affected:

  • (modified) clang/lib/AST/ASTStructuralEquivalence.cpp (+23-1)
  • (modified) clang/test/C/C23/n3037.c (+27)
diff --git a/clang/lib/AST/ASTStructuralEquivalence.cpp b/clang/lib/AST/ASTStructuralEquivalence.cpp
index 0f2762d5c0f14..f113b32d6eb30 100644
--- a/clang/lib/AST/ASTStructuralEquivalence.cpp
+++ b/clang/lib/AST/ASTStructuralEquivalence.cpp
@@ -870,7 +870,29 @@ static bool IsStructurallyEquivalent(StructuralEquivalenceContext &Context,
     else if (T1->getTypeClass() == Type::FunctionNoProto &&
              T2->getTypeClass() == Type::FunctionProto)
       TC = Type::FunctionNoProto;
-    else
+    else if (Context.LangOpts.C23 && !Context.StrictTypeSpelling &&
+             (T1->getTypeClass() == Type::Enum ||
+              T2->getTypeClass() == Type::Enum)) {
+      // In C23, if not being strict about token equivalence, we need to handle
+      // the case where one type is an enumeration and the other type is an
+      // integral type.
+      //
+      // C23 6.7.3.3p16: The enumerated type is compatible with the underlying
+      // type of the enumeration.
+      //
+      // Treat the enumeration as its underlying type and use the builtin type
+      // class comparison.
+      if (T1->getTypeClass() == Type::Enum) {
+        T1 = T1->getAs<EnumType>()->getDecl()->getIntegerType();
+        if (!T2->isBuiltinType() || T1.isNull()) // Sanity check
+          return false;
+      } else if (T2->getTypeClass() == Type::Enum) {
+        T2 = T2->getAs<EnumType>()->getDecl()->getIntegerType();
+        if (!T1->isBuiltinType() || T2.isNull()) // Sanity check
+          return false;
+      }
+      TC = Type::Builtin;
+    } else
       return false;
   }
 
diff --git a/clang/test/C/C23/n3037.c b/clang/test/C/C23/n3037.c
index ce6f4c4ea7acf..54e8ab43ec271 100644
--- a/clang/test/C/C23/n3037.c
+++ b/clang/test/C/C23/n3037.c
@@ -401,3 +401,30 @@ _Static_assert(0 == _Generic(inner_anon_tagged.untagged, struct { int i; } : 1,
 // unions and structures are both RecordDecl objects, whereas EnumDecl is not).
 enum { E_Untagged1 } nontag_enum; // both-note {{previous definition is here}}
 _Static_assert(0 == _Generic(nontag_enum, enum { E_Untagged1 } : 1, default : 0)); // both-error {{redefinition of enumerator 'E_Untagged1'}}
+
+// Test that enumerations are compatible with their underlying type, but still
+// diagnose when "same type" is required rather than merely "compatible type".
+struct GH149965_1 { int h; };
+struct GH149965_2 { int h; };
+void gh149965(void) {
+  extern struct GH149965_1 x1; // c17-note {{previous declaration is here}}
+  extern struct GH149965_2 x2; // c17-note {{previous declaration is here}}
+
+  enum E1 : int { e1 }; // Fixed underlying type
+  enum E2 { e2 };       // Unfixed underlying type, defaults to int in Clang (unsigned in GCC)
+
+  // Both the structure and the variable declarations are fine because only a
+  // compatible type is required, not the same type, because the structures are
+  // declared in different scopes.
+  struct GH149965_1 { enum E1 h; };
+  struct GH149965_2 { enum E2 h; };
+
+  extern struct GH149965_1 x1; // c17-error {{redeclaration of 'x1'}}
+  extern struct GH149965_2 x2; // c17-error {{redeclaration of 'x2'}}
+
+  // However, in the same scope, the same type is required, not just compatible
+  // types.
+  // FIXME: this should be an error in both C17 and C23 mode.
+  struct GH149965_3 { int h; };     // c17-note {{previous definition is here}}
+  struct GH149965_3 { enum E1 h; }; // c17-error {{redefinition of 'GH149965_3'}}
+}

@AaronBallman
Copy link
Collaborator Author

There's no release note because this is covered by the existing one about N3037. Assuming this patch is landed in time, it would be cherry-picked to the 21.x branch (which also has the release note). If it's not cherry-picked, then I'll add a release note to the main branch.

@AaronBallman
Copy link
Collaborator Author

Ping (note #150946 is related but different)

Comment on lines +887 to +888
if (!T2->isBuiltinType() || T1.isNull()) // Sanity check
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the null check be an assert?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, if the underlying type is invalid, I’d expect us to default to int, so it should probably never be null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In general, yes, but given that I want to put this on the release branch with little time for finding fallout, I'd prefer to not assert; we mostly recover by using int as a type, but that's not a guarantee.

How about this for an idea, leave it this way, land the changes, pick the changes to 21.x, once the pick is finished, make this an assert on main?

Copy link
Member

Choose a reason for hiding this comment

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

In general, yes, but given that I want to put this on the release branch with little time for finding fallout, I'd prefer to not assert; we mostly recover by using int as a type, but that's not a guarantee.

How about this for an idea, leave it this way, land the changes, pick the changes to 21.x, once the pick is finished, make this an assert on main?

sgtm

Copy link
Contributor

Choose a reason for hiding this comment

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

How about this for an idea, leave it this way, land the changes, pick the changes to 21.x, once the pick is finished, make this an assert on main?

Yes, please!

@tru tru moved this from Needs Triage to Needs Backport PR in LLVM Release Status Jul 29, 2025
Comment on lines +887 to +888
if (!T2->isBuiltinType() || T1.isNull()) // Sanity check
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, if the underlying type is invalid, I’d expect us to default to int, so it should probably never be null.

@github-project-automation github-project-automation bot moved this from Needs Backport PR to Needs Merge in LLVM Release Status Jul 29, 2025
@tru
Copy link
Collaborator

tru commented Jul 29, 2025

@AaronBallman can you squash this? (release procedure is a bit different so we prefer to get it pre-squashed).

@AaronBallman
Copy link
Collaborator Author

@AaronBallman can you squash this? (release procedure is a bit different so we prefer to get it pre-squashed).

Sure, but I'm not certain we should have a different merge strategy for main vs release branches so I'd like to understand more about what's driving this. Are we planning on switching all contributions to squashing first? Or is this a temporary thing for release branches?

@tru
Copy link
Collaborator

tru commented Jul 29, 2025

@AaronBallman can you squash this? (release procedure is a bit different so we prefer to get it pre-squashed).

Sure, but I'm not certain we should have a different merge strategy for main vs release branches so I'd like to understand more about what's driving this. Are we planning on switching all contributions to squashing first? Or is this a temporary thing for release branches?

We can't use the github squash and merge workflow on the release branch, it makes it so the PR and the commit is not correctly linked. So we use a script to merge PR's from the release branch instead, this script does a straight rebase merge, so when we have multiple commits that are iterative work in a release pr we ask the backporters to squash them. I can also do it if you prefer.

This limitation is only for the release branches and we hope that the bug we have with the github merge button will go away at some point.

@tru
Copy link
Collaborator

tru commented Jul 29, 2025

Oh never mind @AaronBallman I just noticed that this PR was targeting main and not the release branch, ignore my comments! It just ended up in my review queue since it's in the milestone and I didn't pay attention!

@AaronBallman
Copy link
Collaborator Author

We can't use the github squash and merge workflow on the release branch, it makes it so the PR and the commit is not correctly linked.

Thank you for the explanation! That's not a good situation. :-(

Oh never mind @AaronBallman I just noticed that this PR was targeting main and not the release branch, ignore my comments! It just ended up in my review queue since it's in the milestone and I didn't pay attention!

No worries! Now this is all making more sense to me. :-)

@AaronBallman AaronBallman merged commit 315e2e2 into llvm:main Jul 29, 2025
10 checks passed
@github-project-automation github-project-automation bot moved this from Needs Merge to Done in LLVM Release Status Jul 29, 2025
@AaronBallman AaronBallman deleted the aballman-gh149965 branch July 29, 2025 12:21
@AaronBallman
Copy link
Collaborator Author

/cherry-pick 315e2e2

@llvm-ci
Copy link
Collaborator

llvm-ci commented Jul 29, 2025

LLVM Buildbot has detected a new failure on builder clang-hip-vega20 running on hip-vega20-0 while building clang at step 2 "update-annotated-scripts".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/123/builds/24281

Here is the relevant piece of the build log for the reference
Step 2 (update-annotated-scripts) failure: update (failure)
git version 2.34.1
fatal: unable to access 'https://github.com/llvm/llvm-zorg.git/': Could not resolve host: github.com
fatal: unable to access 'https://github.com/llvm/llvm-zorg.git/': Could not resolve host: github.com

@llvmbot
Copy link
Member

llvmbot commented Jul 29, 2025

/pull-request #151137

tru pushed a commit to llvmbot/llvm-project that referenced this pull request Aug 1, 2025
An enumeration is compatible with its underlying type, which means that
code like the following should be accepted:

  struct A { int h; };
  void func() {
    extern struct A x;
    enum E : int { e };
    struct A { enum E h; };
    extern struct A x;
  }

because the structures are declared in different scopes, the two
declarations of 'x' are both compatible.

Note, the structural equivalence checker does not take scope into
account, but that is something the C standard requires. This means we
are accepting code we should be rejecting per the standard, like:

  void func() {
    struct A { int h; };
    extern struct A x;
    enum E : int { e };
    struct A { enum E h; };
    extern struct A x;
  }

Because the structures are declared in the same scope, the type
compatibility rule require the structures to use the same types, not
merely compatible ones.

Fixes llvm#149965

(cherry picked from commit 315e2e2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c23 clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category diverges-from:gcc Does the clang frontend diverge from gcc on this issue rejects-valid
Projects
Development

Successfully merging this pull request may close these issues.

C23 structure and union compatibility should allow for compatible members
6 participants