Skip to content

Commit

Permalink
[Clang] add support for error+warning fn attrs
Browse files Browse the repository at this point in the history
Add support for the GNU C style __attribute__((error(""))) and
__attribute__((warning(""))). These attributes are meant to be put on
declarations of functions whom should not be called.

They are frequently used to provide compile time diagnostics similar to
_Static_assert, but which may rely on non-ICE conditions (ie. relying on
compiler optimizations). This is also similar to diagnose_if function
attribute, but can diagnose after optimizations have been run.

While users may instead simply call undefined functions in such cases to
get a linkage failure from the linker, these provide a much more
ergonomic and actionable diagnostic to users and do so at compile time
rather than at link time. Users instead may be able use inline asm .err
directives.

These are used throughout the Linux kernel in its implementation of
BUILD_BUG and BUILD_BUG_ON macros. These macros generally cannot be
converted to use _Static_assert because many of the parameters are not
ICEs. The Linux kernel still needs to be modified to make use of these
when building with Clang; I have a patch that does so I will send once
this feature is landed.

To do so, we create a new IR level Function attribute, "dontcall" (both
error and warning boil down to one IR Fn Attr).  Then, similar to calls
to inline asm, we attach a !srcloc Metadata node to call sites of such
attributed callees.

The backend diagnoses these during instruction selection, while we still
know that a call is a call (vs say a JMP that's a tail call) in an arch
agnostic manner.

The frontend then reconstructs the SourceLocation from that Metadata,
and determines whether to emit an error or warning based on the callee's
attribute.

Link: https://bugs.llvm.org/show_bug.cgi?id=16428
Link: ClangBuiltLinux/linux#1173

Reviewed By: aaron.ballman

Differential Revision: https://reviews.llvm.org/D106030
  • Loading branch information
nickdesaulniers committed Aug 25, 2021
1 parent cc4bfd7 commit 846e562
Show file tree
Hide file tree
Showing 26 changed files with 418 additions and 1 deletion.
2 changes: 2 additions & 0 deletions clang/docs/ReleaseNotes.rst
Expand Up @@ -103,6 +103,8 @@ C Language Changes in Clang
- Wide multi-characters literals such as ``L'ab'`` that would previously be interpreted as ``L'b'``
are now ill-formed in all language modes. The motivation for this change is outlined in
`P2362 <wg21.link/P2362>`_.
- Support for ``__attribute__((error("")))`` and
``__attribute__((warning("")))`` function attributes have been added.

C++ Language Changes in Clang
-----------------------------
Expand Down
9 changes: 9 additions & 0 deletions clang/include/clang/Basic/Attr.td
Expand Up @@ -3838,3 +3838,12 @@ def EnforceTCBLeaf : InheritableAttr {
let Documentation = [EnforceTCBLeafDocs];
bit InheritEvenIfAlreadyPresent = 1;
}

def Error : InheritableAttr {
let Spellings = [GCC<"error">, GCC<"warning">];
let Accessors = [Accessor<"isError", [GCC<"error">]>,
Accessor<"isWarning", [GCC<"warning">]>];
let Args = [StringArgument<"UserDiagnostic">];
let Subjects = SubjectList<[Function], ErrorDiag>;
let Documentation = [ErrorAttrDocs];
}
26 changes: 26 additions & 0 deletions clang/include/clang/Basic/AttrDocs.td
Expand Up @@ -6071,3 +6071,29 @@ def EnforceTCBLeafDocs : Documentation {
- ``enforce_tcb_leaf(Name)`` indicates that this function is a part of the TCB named ``Name``
}];
}

def ErrorAttrDocs : Documentation {
let Category = DocCatFunction;
let Heading = "error, warning";
let Content = [{
The ``error`` and ``warning`` function attributes can be used to specify a
custom diagnostic to be emitted when a call to such a function is not
eliminated via optimizations. This can be used to create compile time
assertions that depend on optimizations, while providing diagnostics
pointing to precise locations of the call site in the source.

.. code-block:: c++

__attribute__((warning("oh no"))) void dontcall();
void foo() {
if (someCompileTimeAssertionThatsTrue)
dontcall(); // Warning

dontcall(); // Warning

if (someCompileTimeAssertionThatsFalse)
dontcall(); // No Warning
sizeof(dontcall()); // No Warning
}
}];
}
6 changes: 6 additions & 0 deletions clang/include/clang/Basic/DiagnosticFrontendKinds.td
Expand Up @@ -73,6 +73,12 @@ def note_fe_backend_invalid_loc : Note<"could "
def err_fe_backend_unsupported : Error<"%0">, BackendInfo;
def warn_fe_backend_unsupported : Warning<"%0">, BackendInfo;

def err_fe_backend_error_attr :
Error<"call to %0 declared with 'error' attribute: %1">, BackendInfo;
def warn_fe_backend_warning_attr :
Warning<"call to %0 declared with 'warning' attribute: %1">, BackendInfo,
InGroup<BackendWarningAttributes>;

def err_fe_invalid_code_complete_file : Error<
"cannot locate code-completion file %0">, DefaultFatal;
def err_fe_dependency_file_requires_MT : Error<
Expand Down
1 change: 1 addition & 0 deletions clang/include/clang/Basic/DiagnosticGroups.td
Expand Up @@ -1222,6 +1222,7 @@ def BackendOptimizationRemark : DiagGroup<"pass">;
def BackendOptimizationRemarkMissed : DiagGroup<"pass-missed">;
def BackendOptimizationRemarkAnalysis : DiagGroup<"pass-analysis">;
def BackendOptimizationFailure : DiagGroup<"pass-failed">;
def BackendWarningAttributes : DiagGroup<"attribute-warning">;

// Instrumentation based profiling warnings.
def ProfileInstrMissing : DiagGroup<"profile-instr-missing">;
Expand Down
2 changes: 2 additions & 0 deletions clang/include/clang/Sema/Sema.h
Expand Up @@ -3338,6 +3338,8 @@ class Sema final {
const AttributeCommonInfo &CI,
bool BestCase,
MSInheritanceModel Model);
ErrorAttr *mergeErrorAttr(Decl *D, const AttributeCommonInfo &CI,
StringRef NewUserDiagnostic);
FormatAttr *mergeFormatAttr(Decl *D, const AttributeCommonInfo &CI,
IdentifierInfo *Format, int FormatIdx,
int FirstArg);
Expand Down
9 changes: 9 additions & 0 deletions clang/lib/CodeGen/CGCall.cpp
Expand Up @@ -5317,6 +5317,15 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
TargetDecl->hasAttr<MSAllocatorAttr>())
getDebugInfo()->addHeapAllocSiteMetadata(CI, RetTy->getPointeeType(), Loc);

// Add metadata if calling an __attribute__((error(""))) or warning fn.
if (TargetDecl && TargetDecl->hasAttr<ErrorAttr>()) {
llvm::ConstantInt *Line =
llvm::ConstantInt::get(Int32Ty, Loc.getRawEncoding());
llvm::ConstantAsMetadata *MD = llvm::ConstantAsMetadata::get(Line);
llvm::MDTuple *MDT = llvm::MDNode::get(getLLVMContext(), {MD});
CI->setMetadata("srcloc", MDT);
}

// 4. Finish the call.

// If the call doesn't return, finish the basic block and clear the
Expand Down
31 changes: 31 additions & 0 deletions clang/lib/CodeGen/CodeGenAction.cpp
Expand Up @@ -401,6 +401,7 @@ namespace clang {
const llvm::OptimizationRemarkAnalysisAliasing &D);
void OptimizationFailureHandler(
const llvm::DiagnosticInfoOptimizationFailure &D);
void DontCallDiagHandler(const DiagnosticInfoDontCall &D);
};

void BackendConsumer::anchor() {}
Expand Down Expand Up @@ -758,6 +759,33 @@ void BackendConsumer::OptimizationFailureHandler(
EmitOptimizationMessage(D, diag::warn_fe_backend_optimization_failure);
}

void BackendConsumer::DontCallDiagHandler(const DiagnosticInfoDontCall &D) {
if (const Decl *DE = Gen->GetDeclForMangledName(D.getFunctionName()))
if (const auto *FD = dyn_cast<FunctionDecl>(DE)) {
assert(FD->hasAttr<ErrorAttr>() &&
"expected error or warning function attribute");

if (const auto *EA = FD->getAttr<ErrorAttr>()) {
assert((EA->isError() || EA->isWarning()) &&
"ErrorAttr neither error or warning");

SourceLocation LocCookie =
SourceLocation::getFromRawEncoding(D.getLocCookie());

// FIXME: we can't yet diagnose indirect calls. When/if we can, we
// should instead assert that LocCookie.isValid().
if (!LocCookie.isValid())
return;

Diags.Report(LocCookie, EA->isError()
? diag::err_fe_backend_error_attr
: diag::warn_fe_backend_warning_attr)
<< FD << EA->getUserDiagnostic();
}
}
// TODO: assert if DE or FD were nullptr?
}

/// This function is invoked when the backend needs
/// to report something to the user.
void BackendConsumer::DiagnosticHandlerImpl(const DiagnosticInfo &DI) {
Expand Down Expand Up @@ -829,6 +857,9 @@ void BackendConsumer::DiagnosticHandlerImpl(const DiagnosticInfo &DI) {
case llvm::DK_Unsupported:
UnsupportedDiagHandler(cast<DiagnosticInfoUnsupported>(DI));
return;
case llvm::DK_DontCall:
DontCallDiagHandler(cast<DiagnosticInfoDontCall>(DI));
return;
default:
// Plugin IDs are not bound to any value as they are set dynamically.
ComputeDiagRemarkID(Severity, backend_plugin, DiagID);
Expand Down
3 changes: 3 additions & 0 deletions clang/lib/CodeGen/CodeGenModule.cpp
Expand Up @@ -2136,6 +2136,9 @@ void CodeGenModule::SetFunctionAttributes(GlobalDecl GD, llvm::Function *F,
else if (const auto *SA = FD->getAttr<SectionAttr>())
F->setSection(SA->getName());

if (FD->hasAttr<ErrorAttr>())
F->addFnAttr("dontcall");

// If we plan on emitting this inline builtin, we can't treat it as a builtin.
if (FD->isInlineBuiltinDeclaration()) {
const FunctionDecl *FDBody;
Expand Down
10 changes: 10 additions & 0 deletions clang/lib/Sema/SemaDecl.cpp
Expand Up @@ -2628,6 +2628,8 @@ static bool mergeDeclAttribute(Sema &S, NamedDecl *D,
NewAttr = S.mergeDLLImportAttr(D, *ImportA);
else if (const auto *ExportA = dyn_cast<DLLExportAttr>(Attr))
NewAttr = S.mergeDLLExportAttr(D, *ExportA);
else if (const auto *EA = dyn_cast<ErrorAttr>(Attr))
NewAttr = S.mergeErrorAttr(D, *EA, EA->getUserDiagnostic());
else if (const auto *FA = dyn_cast<FormatAttr>(Attr))
NewAttr = S.mergeFormatAttr(D, *FA, FA->getType(), FA->getFormatIdx(),
FA->getFirstArg());
Expand Down Expand Up @@ -3363,6 +3365,14 @@ bool Sema::MergeFunctionDecl(FunctionDecl *New, NamedDecl *&OldD,
New->dropAttr<InternalLinkageAttr>();
}

if (auto *EA = New->getAttr<ErrorAttr>()) {
if (!Old->hasAttr<ErrorAttr>()) {
Diag(EA->getLocation(), diag::err_attribute_missing_on_first_decl) << EA;
Diag(Old->getLocation(), diag::note_previous_declaration);
New->dropAttr<ErrorAttr>();
}
}

if (CheckRedeclarationModuleOwnership(New, Old))
return true;

Expand Down
34 changes: 34 additions & 0 deletions clang/lib/Sema/SemaDeclAttr.cpp
Expand Up @@ -947,6 +947,14 @@ static void handleEnableIfAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
D->addAttr(::new (S.Context) EnableIfAttr(S.Context, AL, Cond, Msg));
}

static void handleErrorAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
StringRef NewUserDiagnostic;
if (!S.checkStringLiteralArgumentAttr(AL, 0, NewUserDiagnostic))
return;
if (ErrorAttr *EA = S.mergeErrorAttr(D, AL, NewUserDiagnostic))
D->addAttr(EA);
}

namespace {
/// Determines if a given Expr references any of the given function's
/// ParmVarDecls, or the function's implicit `this` parameter (if applicable).
Expand Down Expand Up @@ -3458,6 +3466,29 @@ static void handleInitPriorityAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
D->addAttr(::new (S.Context) InitPriorityAttr(S.Context, AL, prioritynum));
}

ErrorAttr *Sema::mergeErrorAttr(Decl *D, const AttributeCommonInfo &CI,
StringRef NewUserDiagnostic) {
if (const auto *EA = D->getAttr<ErrorAttr>()) {
std::string NewAttr = CI.getNormalizedFullName();
assert((NewAttr == "error" || NewAttr == "warning") &&
"unexpected normalized full name");
bool Match = (EA->isError() && NewAttr == "error") ||
(EA->isWarning() && NewAttr == "warning");
if (!Match) {
Diag(EA->getLocation(), diag::err_attributes_are_not_compatible)
<< CI << EA;
Diag(CI.getLoc(), diag::note_conflicting_attribute);
return nullptr;
}
if (EA->getUserDiagnostic() != NewUserDiagnostic) {
Diag(CI.getLoc(), diag::warn_duplicate_attribute) << EA;
Diag(EA->getLoc(), diag::note_previous_attribute);
}
D->dropAttr<ErrorAttr>();
}
return ::new (Context) ErrorAttr(Context, CI, NewUserDiagnostic);
}

FormatAttr *Sema::mergeFormatAttr(Decl *D, const AttributeCommonInfo &CI,
IdentifierInfo *Format, int FormatIdx,
int FirstArg) {
Expand Down Expand Up @@ -7979,6 +8010,9 @@ static void ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D,
case ParsedAttr::AT_EnableIf:
handleEnableIfAttr(S, D, AL);
break;
case ParsedAttr::AT_Error:
handleErrorAttr(S, D, AL);
break;
case ParsedAttr::AT_DiagnoseIf:
handleDiagnoseIfAttr(S, D, AL);
break;
Expand Down
11 changes: 11 additions & 0 deletions clang/test/CodeGen/attr-error.c
@@ -0,0 +1,11 @@
// RUN: %clang_cc1 -emit-llvm -o - %s | FileCheck %s
__attribute__((error("oh no"))) void foo(void);

void bar(void) {
foo();
}

// CHECK: call void @foo(), !srcloc [[SRCLOC:![0-9]+]]
// CHECK: declare{{.*}} void @foo() [[ATTR:#[0-9]+]]
// CHECK: attributes [[ATTR]] = {{{.*}}"dontcall"
// CHECK: [[SRCLOC]] = !{i32 {{[0-9]+}}}
11 changes: 11 additions & 0 deletions clang/test/CodeGen/attr-warning.c
@@ -0,0 +1,11 @@
// RUN: %clang_cc1 -emit-llvm -o - %s | FileCheck %s
__attribute__((warning("oh no"))) void foo(void);

void bar(void) {
foo();
}

// CHECK: call void @foo(), !srcloc [[SRCLOC:![0-9]+]]
// CHECK: declare{{.*}} void @foo() [[ATTR:#[0-9]+]]
// CHECK: attributes [[ATTR]] = {{{.*}}"dontcall"
// CHECK: [[SRCLOC]] = !{i32 {{[0-9]+}}}
22 changes: 22 additions & 0 deletions clang/test/Frontend/backend-attribute-error-warning-optimize.c
@@ -0,0 +1,22 @@
// RUN: %clang_cc1 -O2 -verify -emit-codegen-only %s

__attribute__((error("oh no foo"))) void foo(void);

__attribute__((error("oh no bar"))) void bar(void);

int x(void) {
return 8 % 2 == 1;
}
void baz(void) {
foo(); // expected-error {{call to 'foo' declared with 'error' attribute: oh no foo}}
if (x())
bar();
}

// FIXME: indirect call detection not yet supported.
void (*quux)(void);

void indirect(void) {
quux = foo;
quux();
}
61 changes: 61 additions & 0 deletions clang/test/Frontend/backend-attribute-error-warning.c
@@ -0,0 +1,61 @@
// RUN: %clang_cc1 -verify=expected,enabled -emit-codegen-only %s
// RUN: %clang_cc1 -verify=expected,enabled -emit-codegen-only %s -x c++
// RUN: %clang_cc1 -verify -emit-codegen-only -Wno-attribute-warning %s
// RUN: %clang_cc1 -verify -emit-codegen-only -Wno-attribute-warning %s -x c++

__attribute__((error("oh no foo"))) void foo(void);

__attribute__((error("oh no bar"))) void bar(void);

int x(void) {
return 8 % 2 == 1;
}

__attribute__((warning("oh no quux"))) void quux(void);

__attribute__((error("demangle me"))) void __compiletime_assert_455(void);

__attribute__((error("one"), error("two"))) // expected-warning {{attribute 'error' is already applied with different arguments}}
void // expected-note@-1 {{previous attribute is here}}
duplicate_errors(void);

__attribute__((warning("one"), warning("two"))) // expected-warning {{attribute 'warning' is already applied with different arguments}}
void // expected-note@-1 {{previous attribute is here}}
duplicate_warnings(void);

void baz(void) {
foo(); // expected-error {{call to 'foo' declared with 'error' attribute: oh no foo}}
if (x())
bar(); // expected-error {{call to 'bar' declared with 'error' attribute: oh no bar}}

quux(); // enabled-warning {{call to 'quux' declared with 'warning' attribute: oh no quux}}
__compiletime_assert_455(); // expected-error {{call to '__compiletime_assert_455' declared with 'error' attribute: demangle me}}
duplicate_errors(); // expected-error {{call to 'duplicate_errors' declared with 'error' attribute: two}}
duplicate_warnings(); // enabled-warning {{call to 'duplicate_warnings' declared with 'warning' attribute: two}}
}

#ifdef __cplusplus
template <typename T>
__attribute__((error("demangle me, too")))
T
nocall(T t);

struct Widget {
__attribute__((warning("don't call me!")))
operator int() { return 42; }
};

void baz_cpp(void) {
foo(); // expected-error {{call to 'foo' declared with 'error' attribute: oh no foo}}
if (x())
bar(); // expected-error {{call to 'bar' declared with 'error' attribute: oh no bar}}
quux(); // enabled-warning {{call to 'quux' declared with 'warning' attribute: oh no quux}}

// Test that we demangle correctly in the diagnostic for C++.
__compiletime_assert_455(); // expected-error {{call to '__compiletime_assert_455' declared with 'error' attribute: demangle me}}
nocall<int>(42); // expected-error {{call to 'nocall<int>' declared with 'error' attribute: demangle me, too}}

Widget W;
int w = W; // enabled-warning {{'operator int' declared with 'warning' attribute: don't call me!}}
}
#endif
Expand Up @@ -63,6 +63,7 @@
// CHECK-NEXT: EnforceTCB (SubjectMatchRule_function)
// CHECK-NEXT: EnforceTCBLeaf (SubjectMatchRule_function)
// CHECK-NEXT: EnumExtensibility (SubjectMatchRule_enum)
// CHECK-NEXT: Error (SubjectMatchRule_function)
// CHECK-NEXT: ExcludeFromExplicitInstantiation (SubjectMatchRule_variable, SubjectMatchRule_function, SubjectMatchRule_record)
// CHECK-NEXT: ExternalSourceSymbol ((SubjectMatchRule_record, SubjectMatchRule_enum, SubjectMatchRule_enum_constant, SubjectMatchRule_field, SubjectMatchRule_function, SubjectMatchRule_namespace, SubjectMatchRule_objc_category, SubjectMatchRule_objc_implementation, SubjectMatchRule_objc_interface, SubjectMatchRule_objc_method, SubjectMatchRule_objc_property, SubjectMatchRule_objc_protocol, SubjectMatchRule_record, SubjectMatchRule_type_alias, SubjectMatchRule_variable))
// CHECK-NEXT: FlagEnum (SubjectMatchRule_enum)
Expand Down
40 changes: 40 additions & 0 deletions clang/test/Sema/attr-error.c
@@ -0,0 +1,40 @@
// RUN: %clang_cc1 -fsyntax-only -verify %s
#if !__has_attribute(error)
#error "error attribute missing"
#endif

__attribute__((error("don't call me!"))) int good0(void);

__attribute__((error)) // expected-error {{'error' attribute takes one argument}}
int
bad0(void);

int bad1(__attribute__((error("bad1"))) int param); // expected-error {{'error' attribute only applies to functions}}

int bad2(void) {
__attribute__((error("bad2"))); // expected-error {{'error' attribute cannot be applied to a statement}}
}

__attribute__((error(3))) // expected-error {{'error' attribute requires a string}}
int
bad3(void);

__attribute__((error("foo"), error("foo"))) int good1(void);
__attribute__((error("foo"))) int good1(void);
__attribute__((error("foo"))) int good1(void) {}

__attribute__((error("foo"), warning("foo"))) // expected-error {{'warning' and 'error' attributes are not compatible}}
int
bad4(void);
// expected-note@-3 {{conflicting attribute is here}}

__attribute__((error("foo"))) int bad5(void); // expected-note {{conflicting attribute is here}}
__attribute__((warning("foo"))) int bad5(void); // expected-error {{'error' and 'warning' attributes are not compatible}}

/*
* Note: we differ from GCC here; rather than support redeclarations that add
* or remove this fn attr, we diagnose such differences.
*/

void foo(void); // expected-note {{previous declaration is here}}
__attribute__((error("oh no foo"))) void foo(void); // expected-error {{'error' attribute does not appear on the first declaration}}

0 comments on commit 846e562

Please sign in to comment.