Skip to content
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

[Clang] Emit error for duplicate mangled names within a lambda #107581

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

kmclaughlin-arm
Copy link
Contributor

When functions are passed as arguments to a lambda, it's possible for
the mangled names of these functions to be the same despite the prototypes
being different. For example:

  int non_streaming_fn(int);
  int streaming_fn(int) __arm_streaming;

  auto lambda_fn = [](const auto &f) { return f(); };
  return lambda_fn(non_streaming_fn) + lambda_fn(streaming_fn);

Only one function will be generated for the lambda above and the __arm_streaming
attribute from streaming_fn will be incorrectly applied to the non_streaming_fn call.
With this change, an error will be emitted when a duplicate mangled name is found
but the function prototypes do not match.

When functions are passed as arguments to a lambda, it's possible for
the mangled names of these functions to be the same despite the prototypes
being different. For example:

  int non_streaming_fn(int);
  int streaming_fn(int) __arm_streaming;

  auto lambda_fn = [](const auto &f) { return f(); };
  return lambda_fn(non_streaming_fn) + lambda_fn(streaming_fn);

Only one function will be generated for the lambda above and the __arm_streaming
attribute from streaming_fn will be incorrectly applied to non_streaming_fn.
With this change, an error will be emitted when a duplicate mangled name is
found but the function prototypes do not match.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:codegen labels Sep 6, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Sep 6, 2024

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-codegen

Author: Kerry McLaughlin (kmclaughlin-arm)

Changes

When functions are passed as arguments to a lambda, it's possible for
the mangled names of these functions to be the same despite the prototypes
being different. For example:

  int non_streaming_fn(int);
  int streaming_fn(int) __arm_streaming;

  auto lambda_fn = [](const auto &f) { return f(); };
  return lambda_fn(non_streaming_fn) + lambda_fn(streaming_fn);

Only one function will be generated for the lambda above and the __arm_streaming
attribute from streaming_fn will be incorrectly applied to the non_streaming_fn call.
With this change, an error will be emitted when a duplicate mangled name is found
but the function prototypes do not match.


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

2 Files Affected:

  • (modified) clang/lib/CodeGen/CodeGenModule.cpp (+26-9)
  • (added) clang/test/CodeGenCXX/aarch64-sme-lambda-attributes.cpp (+28)
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index df4c13c9ad97aa..b07090e3de6dfd 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -4638,8 +4638,8 @@ llvm::Constant *CodeGenModule::GetOrCreateLLVMFunction(
   // Lookup the entry, lazily creating it if necessary.
   llvm::GlobalValue *Entry = GetGlobalValue(MangledName);
   if (Entry) {
+    const FunctionDecl *FD = cast_or_null<FunctionDecl>(D);
     if (WeakRefReferences.erase(Entry)) {
-      const FunctionDecl *FD = cast_or_null<FunctionDecl>(D);
       if (FD && !FD->hasAttr<WeakAttr>())
         Entry->setLinkage(llvm::Function::ExternalLinkage);
     }
@@ -4652,18 +4652,35 @@ llvm::Constant *CodeGenModule::GetOrCreateLLVMFunction(
 
     // If there are two attempts to define the same mangled name, issue an
     // error.
-    if (IsForDefinition && !Entry->isDeclaration()) {
-      GlobalDecl OtherGD;
-      // Check that GD is not yet in DiagnosedConflictingDefinitions is required
-      // to make sure that we issue an error only once.
-      if (lookupRepresentativeDecl(MangledName, OtherGD) &&
-          (GD.getCanonicalDecl().getDecl() !=
-           OtherGD.getCanonicalDecl().getDecl()) &&
-          DiagnosedConflictingDefinitions.insert(GD).second) {
+    GlobalDecl OtherGD;
+    // Check that GD is not yet in DiagnosedConflictingDefinitions is required
+    // to make sure that we issue an error only once.
+    if (GD && lookupRepresentativeDecl(MangledName, OtherGD) &&
+        (GD.getCanonicalDecl().getDecl() !=
+         OtherGD.getCanonicalDecl().getDecl()) &&
+        DiagnosedConflictingDefinitions.insert(GD).second) {
+      if (IsForDefinition && !Entry->isDeclaration()) {
         getDiags().Report(D->getLocation(), diag::err_duplicate_mangled_name)
             << MangledName;
         getDiags().Report(OtherGD.getDecl()->getLocation(),
                           diag::note_previous_definition);
+      } else {
+        // For lambdas, it's possible to create the same mangled name from
+        // different function prototypes. For example, two FPTs may have
+        // identical types but incompatible function attributes which we should
+        // not allow.
+        auto *MD = dyn_cast<CXXMethodDecl>(D);
+        if (MD && MD->getParent()->isLambda()) {
+          const FunctionDecl *OtherFD =
+              cast_or_null<FunctionDecl>(OtherGD.getDecl());
+          if (FD && FD->hasPrototype() && OtherFD && OtherFD->hasPrototype()) {
+            if (FD->getType()->getAs<FunctionProtoType>() !=
+                OtherFD->getType()->getAs<FunctionProtoType>())
+              getDiags().Report(D->getLocation(),
+                                diag::err_duplicate_mangled_name)
+                  << MangledName;
+          }
+        }
       }
     }
 
diff --git a/clang/test/CodeGenCXX/aarch64-sme-lambda-attributes.cpp b/clang/test/CodeGenCXX/aarch64-sme-lambda-attributes.cpp
new file mode 100644
index 00000000000000..bce3e657001679
--- /dev/null
+++ b/clang/test/CodeGenCXX/aarch64-sme-lambda-attributes.cpp
@@ -0,0 +1,28 @@
+// REQUIRES: aarch64-registered-target
+
+// RUN: %clang_cc1 -triple aarch64 -target-feature +sme -target-feature +sme2 -emit-llvm -o - %s -verify -DTEST1
+// RUN: %clang_cc1 -triple aarch64 -target-feature +sme -target-feature +sme2 -emit-llvm -o - %s -verify -DTEST2
+
+int normal_fn(int);
+int streaming_fn(int) __arm_streaming;
+int streaming_compatible_fn(int) __arm_streaming_compatible;
+
+#ifdef TEST1
+
+// expected-error@+2 {{definition with same mangled name '_ZZ32function_params_normal_streamingvENK3$_0clIFiiEEEDaRT_' as another definition}}
+int function_params_normal_streaming() {
+  auto a = [](auto &fn) { return fn(42); };
+  return a(normal_fn) + a(streaming_fn);
+}
+
+#endif
+
+#ifdef TEST2
+
+// expected-error@+2 {{definition with same mangled name '_ZZ36function_params_streaming_compatiblevENK3$_0clIFiiEEEDaRT_' as another definition}}
+int function_params_streaming_compatible() {
+  auto a = [](auto &fn) { return fn(42); };
+  return a(streaming_fn) + a(streaming_compatible_fn);
+}
+
+#endif

@efriedma-quic
Copy link
Collaborator

Isn't this a bug in the mangler? I mean, it's better to print an error rather than silently miscompile, but this doesn't really solve the issue.

@kmclaughlin-arm
Copy link
Contributor Author

kmclaughlin-arm commented Sep 9, 2024

Isn't this a bug in the mangler? I mean, it's better to print an error rather than silently miscompile, but this doesn't really solve the issue.

Hi @efriedma-quic,
The SME type attributes are not part of the name mangling. We figured an error message would be useful in general to avoid Clang generating incorrect code for this case and any future cases where the types don't match (but the mangled names are the same).
I did start trying to fix this by adding some special name mangling for lambdas in particular which took the attributes into account, but doing this required some SME specific changes in the general mangling code which made us unsure whether this approach was desirable.

@efriedma-quic
Copy link
Collaborator

The SME type attributes are not part of the name mangling

If int(&)() __arm_streaming is a different type from int(&)() for template instantiation, it should have different mangling. If it doesn't, that's a bug. If there is no spec for the correct mangling, someone should write a spec.

@efriedma-quic
Copy link
Collaborator

(You shouldn't need to special-case lambdas; any use of the type needs the appropriate mangling, except maybe the function declaration itself.)

@sdesmalen-arm
Copy link
Collaborator

The SME type attributes are not part of the name mangling

If int(&)() __arm_streaming is a different type from int(&)() for template instantiation, it should have different mangling. If it doesn't, that's a bug. If there is no spec for the correct mangling, someone should write a spec.

I agree that it would be better for the SME attributes to be represented in the type mangling, although changing the mangling at this point would be an ABI break. I'll probe and follow this up!

Nevertheless I believe there is merit to this patch because Clang should not silently generate wrong code. For other instantiations Clang just emits a diagnostic, e.g.

void fn(int);
void fs(int) __arm_streaming;

template <typename F, typename... Args>
void dispatcher(F f, Args... args) {
  f(args...);
}

void foo() {
  dispatcher(fn, 41);
  dispatcher(fs, 42);

Results in a diagnostic error: definition with same mangled name '_Z10dispatcherIPFviEJiEEvT_DpT0_' as another definition. The fact that a similar diagnostic is not produced for lambda instantiations seems like something that needs fixing.

@efriedma-quic
Copy link
Collaborator

Sure, it makes sense to print a diagnostic for lambdas.


I'm having a bit of trouble understanding the way the new code is structured. What makes the definition of lambda call operators special here? Do we not call GetOrCreateLLVMFunction with IsForDefinition set?

@kmclaughlin-arm
Copy link
Contributor Author

kmclaughlin-arm commented Sep 13, 2024

I'm having a bit of trouble understanding the way the new code is structured. What makes the definition of lambda call operators special here? Do we not call GetOrCreateLLVMFunction with IsForDefinition set?

When I added this error I incorrectly thought GetOrCreateLLVMFunction is not called with IsForDefinition set. Now that I've taken another look at it, I see that it is set and the reason the existing error is not emitted is because of the !Entry->isDeclaration() condition.

I have removed most of my changes and instead extended the isDeclaration() check to also consider whether this is lambda. I don't think it's correct to remove isDeclaration(); I believe this was added to allow cases such as a non-C++ declaration with a mangled name matching a C++ function (described in https://reviews.llvm.org/D11297).

Comment on lines 4674 to 4678
const FunctionDecl *OtherFD =
cast_or_null<FunctionDecl>(OtherGD.getDecl());
if (FD && FD->hasPrototype() && OtherFD && OtherFD->hasPrototype()) {
if (FD->getType()->getAs<FunctionProtoType>() !=
OtherFD->getType()->getAs<FunctionProtoType>())
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand why there's a need to check the FunctionProtoTypes here, because the check GD.getCanonicalDecl().getDecl() != OtherGD.getCanonicalDecl().getDecl() has already checked if the declarations and types are the same.

Note that I tried a smaller change like this:

bool IsLambdaInstantiation =
    D && isa<CXXMethodDecl>(D) &&     
    cast<CXXMethodDecl>(D)->getParent()->isLambda();
if (IsForDefinition && (!Entry->isDeclaration() || IsLambdaInstantiation)) {

And got the desired result. Would the above be sufficient or is there something this is not covering?

Copy link
Collaborator

Choose a reason for hiding this comment

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

(I see you implemented the same suggestion, not sure why Github didn't publish my comment)

Comment on lines 21 to 30
#ifdef TEST2

// expected-error@+3 {{definition with same mangled name '_ZZ36function_params_streaming_compatiblevENK3$_0clIFiiEEEDaRT_' as another definition}}
// expected-note@+2 {{previous definition is here}}
int function_params_streaming_compatible() {
auto a = [](auto &fn) { return fn(42); };
return a(streaming_fn) + a(streaming_compatible_fn);
}

#endif
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test adds much value over the other test above, because the logic is no different for streaming and streaming-compatible attributes, so I think you can remove it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a separate test for a non-SME attribute that has a similar issue?

Type attributes for the calling convention are also not part of the type mangling, e.g. https://godbolt.org/z/sEdsGr53K which would (without your change) miscompile.

- Added a test using calling convention attributes
Comment on lines 3 to 4
// RUN: %clang_cc1 -triple aarch64 -emit-llvm -o - %s -verify -DTEST1
// RUN: %clang_cc1 -triple aarch64 -target-feature +sme -target-feature +sme2 -emit-llvm -o - %s -verify -DTEST2
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you now change the filename, since it's now wider scope than just the SME attributes. Maybe lambda-instantiation-require-different-mangled-names.cpp or something?

The same issue exists for more generic ways to specify the calling convention, e.g. __attribute__((preserve_none)), which also affects other targets. For these, the compiler could select the worst-case calling convention for its instantiation (i.e. assume everything is clobbered), rather than emitting a diagnostic. But that would require explicit logic to select the right calling convention, which is out of scope of this PR.

nit: rather than using ifdef's, I'd recommend using the split-file tool instead (it does mean duplicating the normal_fn declaration)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed the ifdefs & renamed this to lambda-instantiation-mangling-conflicts.cpp as it was a bit shorter, but I can rename to lambda-instantiation-require-different-mangled-names.cpp if preferred.

@efriedma-quic
Copy link
Collaborator

I see that it is set and the reason the existing error is not emitted is because of the !Entry->isDeclaration() condition.

For non-lambda methods, the way this works it that we call GetOrCreateLLVMFunction for both methods... for the first method, the !Entry->isDeclaration() would be false, but for the second one, it would be true because we've emitted the definition of the first method. What's different for lambda methods?

…angling-conflicts.cpp

- Replaced ifdefs in test with use of split-file
@kmclaughlin-arm
Copy link
Contributor Author

For non-lambda methods, the way this works it that we call GetOrCreateLLVMFunction for both methods... for the first method, the !Entry->isDeclaration() would be false, but for the second one, it would be true because we've emitted the definition of the first method. What's different for lambda methods?

I don't think lambda methods are any different to what you have described, but I think my previous explanation was unclear. What I found is that IsForDefinition is set only the second time GetOrCreateLLVMFunction is called, where Entry->isDeclaration() is also true.

@efriedma-quic
Copy link
Collaborator

In the example in #107581 (comment) , there are four relevant calls to GetOrCreateLLVMFunction: one for each function with IsInDefinition false, and one for each function with IsInDefinition true. The last of those calls triggers the diagnostic

For the lambda example, there are only three relevant calls to GetOrCreateLLVMFunction; one for each function with IsInDefinition false, but then only one with IsInDefinition true.

It's not clear to me why the two cases are different, and I don't really want to add lambda-specific check without knowing why lambdas are special here. Maybe the right condition to check is something else.

@kmclaughlin-arm
Copy link
Contributor Author

For the lambda example, there are only three relevant calls to GetOrCreateLLVMFunction; one for each function with IsInDefinition false, but then only one with IsInDefinition true.

It's not clear to me why the two cases are different, and I don't really want to add lambda-specific check without knowing why lambdas are special here. Maybe the right condition to check is something else.

From what I've observed comparing the lambda and template test cases, the difference comes from how we create the list of Decls which can be deferred.

Both functions foo() from the template case in #107581 (comment) and function_params_normal_streaming() from the lambda test case in this PR cannot be deferred by EmitGlobal here and must be emitted straight away (by calling GetOrCreateLLVMFunction):

 if (MustBeEmitted(Global) && MayBeEmittedEagerly(Global)) {
    EmitGlobalDefinition(GD);
    addEmittedDeferredDecl(GD);
    return;

For the template case this happens first, before calling EmitGlobal for dispatcher(). For lambdas the function arguments of the lambda are handled first. This changes whether the name is found later in EmitGlobal when adding the MangledName to the DeferredDeclsToEmit list:

  StringRef MangledName = getMangledName(GD);
    if (GetGlobalValue(MangledName) != nullptr) {
      // The value has already been used and should therefore be emitted.
      addDeferredDeclToEmit(GD);
    ...
    } else {
      // Otherwise, remember that we saw a deferred decl with this name.  The
      // first use of the mangled name will cause it to move into
      // DeferredDeclsToEmit.
      DeferredDecls[MangledName] = GD;
    }

Although we'll set DeferredDecls with the mangled name from the lambda twice, it will only appear in DeferredDeclsToEmit once. In the other example, it will be added to the DeferredDeclsToEmit list with addDeferredDeclToEmit both times. When EmitDeferred is called later it uses this list and calls GetOrCreateLLVMFunction with IsForDefinition set to true.

I'm not sure adding a check specificially for lambdas is the right thing to do because of this. I considered trying to emit the error somewhere in lib/Sema, however I think this is too early to check whether functions will have the same mangled name.

Something I'm considering instead is to try and emit the error in EmitGlobal, where we set DeferredDecls for a given MangledName (around DeferredDecls[MangledName] = GD in the snippet above). I think this could be a better place to emit the error as we can look for whether GD has already been assigned to MangledName. I'm still investigating how to do this without causing failures in some of the existing tests which are using lambdas & templates.

I will also be away for the next two weeks but plan to pick this up again when I return.

@efriedma-quic
Copy link
Collaborator

Oh, that makes sense... so the issue is generally with functions we emit lazily?

I'd say it's reasonable to emit an error if we have two definitions for the same symbol, even if we don't end up emitting them because they're deferred. Lazy emission is an optimization, and we don't guarantee whether a particular odr-used function will be emitted.

@kmclaughlin-arm
Copy link
Contributor Author

Oh, that makes sense... so the issue is generally with functions we emit lazily?
I'd say it's reasonable to emit an error if we have two definitions for the same symbol, even if we don't end up emitting them because they're deferred.

Yes, I believe so. I've moved the error to EmitGlobal at the point where DeferredDecls is updated if a duplicate mangled name is found. I've tried to exclude the cases where this can be expected (such as for multiversioning where the mangled name may be changed later by UpdateMultiVersionNames), but there is nothing specific to lambdas now.

I agree that it would be better for the SME attributes to be represented in the type mangling, although changing the mangling at this point would be an ABI break. I'll probe and follow this up!

Following on from this, I am also working on changes to name mangling of types which have SME attributes (ARM-software/abi-aa#290)

@efriedma-quic
Copy link
Collaborator

Can you add a testcase that doesn't involve lambdas? Maybe something like the following:

inline void f(int) asm("foo");
inline void f(int) {}
inline void f() asm("foo");
inline void f(){}

Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:codegen clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants