From 3b07cc625be21d3addaaa7ef190054e36a55a1fd Mon Sep 17 00:00:00 2001 From: Petar Kirov Date: Wed, 10 Jun 2026 21:08:27 +0300 Subject: [PATCH] Fix #23224 - ImportC: parse C23 [[...]] attribute-specifier-sequences ImportC now understands the C23 attribute syntax (C23 6.7.13) wherever a GNU __attribute__ is accepted: on declarations, pointers, struct/union/enum tags, struct members, enumerators, function parameters, and statements. [[deprecated]]/[[deprecated("msg")]] (6.7.13.5) maps to D's deprecated, and [[deprecated]] on an enumerator deprecates the D enum member. [[noreturn]]/ [[_Noreturn]] (6.7.13.7) sets the function's noreturn flag (a backend codegen hint, not D's noreturn type). The remaining standard attributes ([[nodiscard]], [[maybe_unused]], [[fallthrough]], [[unsequenced]], [[reproducible]]), vendor-prefixed (gnu::, clang::) and unknown attributes are accepted and ignored. A standard attribute spelled attr and __attr__ are treated identically (6.7.13.1). --- changelog/dmd.importc-c23-attributes.dd | 27 ++ compiler/src/dmd/cparse.d | 266 +++++++++++++++--- compiler/test/compilable/c23attributes.c | 82 ++++++ compiler/test/compilable/ctests2.c | 8 + compiler/test/compilable/testc23attributes.d | 21 ++ .../c23attributes_malformed.c | 14 + 6 files changed, 379 insertions(+), 39 deletions(-) create mode 100644 changelog/dmd.importc-c23-attributes.dd create mode 100644 compiler/test/compilable/c23attributes.c create mode 100644 compiler/test/compilable/testc23attributes.d create mode 100644 compiler/test/fail_compilation/c23attributes_malformed.c diff --git a/changelog/dmd.importc-c23-attributes.dd b/changelog/dmd.importc-c23-attributes.dd new file mode 100644 index 000000000000..840fea395958 --- /dev/null +++ b/changelog/dmd.importc-c23-attributes.dd @@ -0,0 +1,27 @@ +ImportC now parses C23 `[[...]]` attribute syntax + +You can now use C23 attributes in C files imported with ImportC, anywhere a GNU +`__attribute__` was already accepted: on declarations, pointers, struct/union/enum +tags, struct members, enumerators, function parameters, and statements. + +`[[deprecated]]` (with an optional message) marks a function, variable, type or enum +member as deprecated in D, and `[[noreturn]]` flags a function as non-returning, just +like GNU `__attribute__((noreturn))`. Every other standard attribute (such as +`[[nodiscard]]`, `[[maybe_unused]]` and +`[[fallthrough]]`), as well as vendor-prefixed ones like `[[gnu::...]]` and +`[[clang::...]]` and any unknown attribute, is parsed and ignored, so existing C23 +headers keep compiling. The `attr` and `__attr__` spellings are equivalent (C23 6.7.13). + +------- +[[deprecated("use bar instead")]] int foo(void); +[[nodiscard]] int bar(void); + +struct [[deprecated]] S +{ + [[maybe_unused]] int a; +}; + +enum E { A [[deprecated]], B [[maybe_unused]] = 5 }; + +int baz(int x [[maybe_unused]]); +------- diff --git a/compiler/src/dmd/cparse.d b/compiler/src/dmd/cparse.d index 0ca40be20743..c0efa5aa4717 100644 --- a/compiler/src/dmd/cparse.d +++ b/compiler/src/dmd/cparse.d @@ -400,6 +400,21 @@ final class CParser(AST) : Parser!AST break; } + case TOK.leftBracket: + // C23 6.7.13 attribute-specifier-sequence prefixing a declaration or statement + if (!isCAttributeStart()) + goto default; + { + Specifier attrspec; + attrspec.packalign.setDefault(); + cparseCAttributes(attrspec); + if (startsDeclaration(&token)) + goto Ldeclaration; + // attributes appertain to the following statement + s = cparseStatement(0); + } + break; + case TOK._Static_assert: // _Static_assert ( constant-expression, string-literal ) ; s = new AST.StaticAssertStatement(cparseStaticAssert()); break; @@ -1888,8 +1903,8 @@ final class CParser(AST) : Parser!AST */ first = false; } - else if (token.value == TOK.__attribute__) - cparseGnuAttributes(specifier); + else if (token.value == TOK.__attribute__ || isCAttributeStart()) + cparseAttributes(specifier); // GNU / C23 6.7.13 else break; } @@ -2517,6 +2532,14 @@ final class CParser(AST) : Parser!AST break; } + case TOK.leftBracket: // C23 6.7.13 attribute-specifier-sequence + { + if (!isCAttributeStart()) + goto default; + cparseCAttributes(specifier); + break; + } + case TOK.__declspec: { /* Microsoft extension @@ -2923,8 +2946,7 @@ final class CParser(AST) : Parser!AST const mod = cparseTypeQualifierList(); if (mod & MOD.xconst) constTypes.push(t); - if (token.value == TOK.__attribute__) - cparseGnuAttributes(specifier); + cparseAttributes(specifier); // GNU / C23 6.7.13 continue; default: @@ -2954,6 +2976,13 @@ final class CParser(AST) : Parser!AST { case TOK.leftBracket: { + if (isCAttributeStart()) + { + // C23 6.7.13 attribute-specifier-sequence appertaining to + // the declarator, e.g. `int x [[maybe_unused]]` + cparseCAttributes(specifier); + continue; + } // post [] syntax, pick up any leading type qualifiers, `static` and `*` AST.Type ta; nextToken(); @@ -3264,8 +3293,7 @@ final class CParser(AST) : Parser!AST AST.ArgumentLabel id; auto t = cparseDeclarator(DTR.xparameter, tspec, id, specifier); - if (token.value == TOK.__attribute__) - cparseGnuAttributes(specifier); + cparseAttributes(specifier); // GNU / C23 6.7.13 if (specifier.mod & MOD.xconst) t = toConst(t); auto param = new AST.Parameter(id.name ? id.loc : typeLoc, @@ -3860,6 +3888,169 @@ final class CParser(AST) : Parser!AST } } + /************************* + * C23 6.7.13 attribute-specifier-sequence parser. + * + * Grammar (N3220 6.7.13.2): + * attribute-specifier-sequence: + * attribute-specifier-sequence(opt) attribute-specifier + * attribute-specifier: + * [ [ attribute-list ] ] + * attribute-list: + * attribute(opt) + * attribute-list , attribute(opt) + * + * The lexer emits two `TOK.leftBracket` for `[[` and two `TOK.rightBracket` + * for `]]`; there is no combined token. + * Params: + * specifier = filled in with the recognized attribute(s) + */ + private void cparseCAttributes(ref Specifier specifier) + { + while (isCAttributeStart()) + { + nextToken(); // first `[` + nextToken(); // second `[` + if (token.value != TOK.rightBracket) + { + while (1) + { + cparseCAttribute(specifier); + if (token.value != TOK.comma) + break; + nextToken(); + } + } + check(TOK.rightBracket); + check(TOK.rightBracket); + } + } + + /************************* + * True if positioned at the start of a C23 `[[` attribute-specifier-sequence. + * A lone `[` (e.g. an array subscript) is not one. + */ + private bool isCAttributeStart() + { + return token.value == TOK.leftBracket && peekNext() == TOK.leftBracket; + } + + /************************* + * Parse a single C23 attribute (N3220 6.7.13.2): + * attribute: + * attribute-token attribute-argument-clause(opt) + * attribute-token: + * standard-attribute // identifier + * attribute-prefixed-token // attribute-prefix :: identifier + * attribute-argument-clause: + * ( balanced-token-sequence(opt) ) + * + * `deprecated` (6.7.13.5) and `noreturn`/`_Noreturn` (6.7.13.7) are mapped onto + * the existing Specifier fields; every other standard attribute + * (`nodiscard` 6.7.13.3, `maybe_unused` 6.7.13.4, `fallthrough` 6.7.13.6, + * `unsequenced`/`reproducible` 6.7.13.8), prefixed attribute, or unknown + * attribute is accepted and ignored (6.7.13.1). The argument clause is a + * balanced-token-sequence, not an expression, so it is skipped by paren + * balancing except for the `deprecated` message string. + * Params: + * specifier = filled in with the recognized attribute(s) + */ + private void cparseCAttribute(ref Specifier specifier) + { + // C23 6.7.13.2: a keyword used as an attribute-token is treated as an + // identifier; `_Noreturn` is the only such keyword with a mapping. + if (token.value == TOK._Noreturn) + { + specifier.noreturn = true; // C23 6.7.13.7 + nextToken(); + if (token.value == TOK.leftParenthesis) + cparseParens(); + return; + } + if (token.value != TOK.identifier) + { + // empty attribute (e.g. a trailing comma) or some other keyword used as + // an attribute-token: accept and ignore + if (isGnuAttributeName()) + { + nextToken(); + if (token.value == TOK.leftParenthesis) + cparseParens(); + } + return; + } + + Identifier ident = token.ident; + nextToken(); + + // attribute-prefixed-token: attribute-prefix :: identifier + // implementation-specific (e.g. `gnu::`, `clang::`): accept and ignore + if (token.value == TOK.colonColon) + { + nextToken(); + if (token.value == TOK.identifier || isGnuAttributeName()) + nextToken(); + if (token.value == TOK.leftParenthesis) + cparseParens(); + return; + } + + // standard attribute; normalize the `__attr__` spelling (C23 6.7.13.1) + const id = normalizeCAttributeName(ident); + if (id is Id._deprecated) // C23 6.7.13.5 + { + specifier._deprecated = true; + if (token.value == TOK.leftParenthesis) // optional message string + { + nextToken(); + specifier.depMsg = cparseExpression(); + check(TOK.rightParenthesis); + } + return; + } + if (id is Id.noreturn) // C23 6.7.13.7 + { + specifier.noreturn = true; + if (token.value == TOK.leftParenthesis) + cparseParens(); + return; + } + + // C23 6.7.13.3/.4/.6/.8 and any unknown attribute: accept and ignore + if (token.value == TOK.leftParenthesis) + cparseParens(); + } + + /************************* + * C23 6.7.13.1: a standard attribute spelled `attr` and the form `__attr__` + * behave identically. Strip a surrounding `__` pair so the core name can be + * compared against the known attribute identifiers. + */ + private Identifier normalizeCAttributeName(Identifier ident) + { + const s = ident.toString(); + if (s.length > 4 && s[0] == '_' && s[1] == '_' && s[$ - 1] == '_' && s[$ - 2] == '_') + return Identifier.idPool(s[2 .. $ - 2]); + return ident; + } + + /************************* + * Consume any mixture of GNU `__attribute__((...))` and C23 `[[...]]` + * attribute-specifier-sequences, in any order, into `specifier`. + */ + private void cparseAttributes(ref Specifier specifier) + { + while (1) + { + if (token.value == TOK.__attribute__) + cparseGnuAttributes(specifier); + else if (isCAttributeStart()) + cparseCAttributes(specifier); + else + break; + } + } + //} /******************************************************************************/ /***************************** Struct & Enum Parser ***************************/ @@ -3901,8 +4092,7 @@ final class CParser(AST) : Parser!AST */ Specifier specifier; specifier.packalign.setDefault(); - if (token.value == TOK.__attribute__) - cparseGnuAttributes(specifier); + cparseAttributes(specifier); // GNU / C23 6.7.13 Identifier tag; if (token.value == TOK.identifier) @@ -3943,15 +4133,14 @@ final class CParser(AST) : Parser!AST nextToken(); auto mloc = token.loc; - if (token.value == TOK.__attribute__) - { - /* gnu-attributes can appear here, but just scan and ignore them - * https://gcc.gnu.org/onlinedocs/gcc/Enumerator-Attributes.html - */ - Specifier specifierx; - specifierx.packalign.setDefault(); - cparseGnuAttributes(specifierx); - } + /* gnu-attributes / C23 6.7.13 attributes can appear before and after the + * (optional) value (https://gcc.gnu.org/onlinedocs/gcc/Enumerator-Attributes.html). + * `deprecated` (C23 6.7.13.5) is applied to the enumerator, since D + * supports deprecated enum members; the rest are accepted and ignored. + */ + Specifier emspec; + emspec.packalign.setDefault(); + cparseAttributes(emspec); AST.Expression value; if (token.value == TOK.assign) @@ -3961,17 +4150,17 @@ final class CParser(AST) : Parser!AST // TODO C11 6.7.2.2-2 value must fit into an int } - if (token.value == TOK.__attribute__) + cparseAttributes(emspec); + + STC emstc = STC.none; + AST.DeprecatedDeclaration dd; + if (emspec._deprecated) // C23 6.7.13.5 { - /* gnu-attributes can appear here, but just scan and ignore them - * https://gcc.gnu.org/onlinedocs/gcc/Enumerator-Attributes.html - */ - Specifier specifierx; - specifierx.packalign.setDefault(); - cparseGnuAttributes(specifierx); + emstc |= STC.deprecated_; + if (emspec.depMsg) + dd = new AST.DeprecatedDeclaration(emspec.depMsg, null); } - - auto em = new AST.EnumMember(mloc, ident, value, null, STC.none, null, null); + auto em = new AST.EnumMember(mloc, ident, value, null, emstc, null, dd); members.push(em); if (token.value == TOK.comma) @@ -3983,11 +4172,10 @@ final class CParser(AST) : Parser!AST } check(TOK.rightCurly); - /* GNU Extensions - * Parse the postfix gnu-attributes (opt) + /* GNU Extensions / C23 6.7.13 + * Parse the postfix attributes (opt) */ - if (token.value == TOK.__attribute__) - cparseGnuAttributes(specifier); + cparseAttributes(specifier); } else if (!tag) error("missing `identifier` after `enum`"); @@ -4035,6 +4223,8 @@ final class CParser(AST) : Parser!AST { if (token.value == TOK.__attribute__) cparseGnuAttributes(tagSpecifier); + else if (isCAttributeStart()) // C23 6.7.13 + cparseCAttributes(tagSpecifier); else if (token.value == TOK.__declspec) cparseDeclspec(tagSpecifier); else if (token.value == TOK.__pragma) @@ -4072,11 +4262,10 @@ final class CParser(AST) : Parser!AST */ } - /* GNU Extensions - * Parse the postfix gnu-attributes (opt) + /* GNU Extensions / C23 6.7.13 + * Parse the postfix attributes (opt) */ - if (token.value == TOK.__attribute__) - cparseGnuAttributes(tagSpecifier); + cparseAttributes(tagSpecifier); if (!tagSpecifier.packalign.isUnknown) { foreach (ref d; (*members)[]) @@ -4232,13 +4421,12 @@ final class CParser(AST) : Parser!AST width = cparseConstantExp(); } - /* GNU Extensions + /* GNU Extensions / C23 6.7.13 * struct-declarator: - * declarator gnu-attributes (opt) - * declarator (opt) : constant-expression gnu-attributes (opt) + * declarator attributes (opt) + * declarator (opt) : constant-expression attributes (opt) */ - if (token.value == TOK.__attribute__) - cparseGnuAttributes(specifier); + cparseAttributes(specifier); if (!tspec && !specifier.scw && !specifier.mod) error("specifier-qualifier-list required"); diff --git a/compiler/test/compilable/c23attributes.c b/compiler/test/compilable/c23attributes.c new file mode 100644 index 000000000000..d930d475cca1 --- /dev/null +++ b/compiler/test/compilable/c23attributes.c @@ -0,0 +1,82 @@ +// C23 attribute-specifier-sequences in ImportC: every form below must parse. +// `deprecated` maps to D's `deprecated`; `noreturn` sets the function's `noreturn` flag +// (a backend codegen hint, not D's `noreturn` type — see dmd PR #12966, which added +// ImportC `__attribute__((noreturn))` this way). Every other standard, prefixed, or +// unknown attribute is accepted and ignored. The `attr` and `__attr__` spellings are +// equivalent. +// +// Spec references (N3220): +// 6.7.13 attribute-specifier-sequence syntax and placement +// 6.7.13.1 standard vs prefixed attributes; the `__attr__` spelling +// 6.7.13.2 attribute / attribute-list grammar; keyword attribute-tokens +// 6.7.13.5 deprecated 6.7.13.7 noreturn / _Noreturn +// 6.7.13.3 nodiscard 6.7.13.4 maybe_unused +// 6.7.13.6 fallthrough 6.7.13.8 unsequenced / reproducible +// 6.7.3.3 deprecated on an enumerator + +// deprecated, with and without a message, incl. the __attr__ spelling +[[deprecated]] int d1(void); +[[deprecated("use d1")]] int d2(void); +[[__deprecated__]] int d3(void); + +// noreturn / _Noreturn (a keyword used as an attribute-token) +[[noreturn]] void nr1(void); +[[_Noreturn]] void nr2(void); +[[__noreturn__]] void nr3(void); + +// accepted and ignored +[[nodiscard]] int nd1(void); +[[nodiscard("keep it")]] int nd2(void); +[[maybe_unused]] static int mu1; +[[unsequenced]] int us1(void); +[[reproducible]] int rp1(void); + +// attribute-prefixed-token (implementation-specific): accepted and ignored +[[gnu::always_inline]] int gp1(void); +[[clang::no_sanitize("address")]] int gp2(void); + +// empty attribute specifier, empty attribute list, and a multi-attribute list +[[]] int e1(void); +[[,]] int e2(void); +[[deprecated, maybe_unused]] int m1(void); + +// an attribute-specifier-sequence is one or more adjacent [[...]] specifiers +[[deprecated]] [[maybe_unused]] int d4(void); + +// on a pointer declarator +int * [[gnu::aligned(8)]] p1; + +// on a struct tag, before a member, and after a member declarator +struct [[deprecated("old")]] S +{ + [[maybe_unused]] int a; + int b [[maybe_unused]]; +}; + +// on enumerators (and a trailing comma) +enum E +{ + A [[deprecated("x")]], + B [[maybe_unused]] = 5, + C, +}; + +// on function parameters, both before and after the declarator +int f1(int x [[maybe_unused]], [[maybe_unused]] int y); + +// at statement scope +int g(void) +{ + [[maybe_unused]] int local = 0; // on a block-scope declaration + switch (local) + { + case 0: + ++local; + [[fallthrough]]; // on a null statement + case 1: + break; + default: + break; + } + return local; +} diff --git a/compiler/test/compilable/ctests2.c b/compiler/test/compilable/ctests2.c index 16da88ba378b..e4fffc043330 100644 --- a/compiler/test/compilable/ctests2.c +++ b/compiler/test/compilable/ctests2.c @@ -1,3 +1,11 @@ +/* +REQUIRED_ARGS: -verrors=simple +TEST_OUTPUT: +--- +compilable/ctests2.c(34): Deprecation: enum member `ctests2.E.oldval` is deprecated +compilable/ctests2.c(27): `oldval` is declared here +--- +*/ /*************************************************/ // https://issues.dlang.org/show_bug.cgi?id=22304 diff --git a/compiler/test/compilable/testc23attributes.d b/compiler/test/compilable/testc23attributes.d new file mode 100644 index 000000000000..01fc2e46460e --- /dev/null +++ b/compiler/test/compilable/testc23attributes.d @@ -0,0 +1,21 @@ +// EXTRA_FILES: c23attributes.c + +// Compile-time verification of the D-observable effect of the C23 [[...]] attributes +// parsed from c23attributes.c. Only `deprecated` (C23 6.7.13.5) surfaces to the D +// frontend, via __traits(isDeprecated); the other standard attributes are accepted and +// ignored, and `noreturn` (6.7.13.7) is a backend codegen hint not visible to D. + +import c23attributes; + +// deprecated on functions: plain, with a message, the __attr__ spelling (6.7.13.1), +// within a multi-attribute list, and across two adjacent attribute-specifiers +static assert(__traits(isDeprecated, d1)); +static assert(__traits(isDeprecated, d2)); +static assert(__traits(isDeprecated, d3)); +static assert(__traits(isDeprecated, m1)); +static assert(__traits(isDeprecated, d4)); + +// C23 6.7.13.5 / 6.7.3.3 -- deprecated on an enumerator is applied to the D enum member; +// a member without it is not deprecated +static assert( __traits(isDeprecated, E.A)); +static assert(!__traits(isDeprecated, E.B)); diff --git a/compiler/test/fail_compilation/c23attributes_malformed.c b/compiler/test/fail_compilation/c23attributes_malformed.c new file mode 100644 index 000000000000..3323735672df --- /dev/null +++ b/compiler/test/fail_compilation/c23attributes_malformed.c @@ -0,0 +1,14 @@ +/* REQUIRED_ARGS: -verrors=simple +TEST_OUTPUT: +--- +fail_compilation/c23attributes_malformed.c(14): Error: found `int` when expecting `]` +fail_compilation/c23attributes_malformed.c(14): Error: expected identifier for declarator +fail_compilation/c23attributes_malformed.c(14): Error: expected identifier for declaration +--- +*/ + +// C23 6.7.13.2: an attribute-specifier is `[ [ attribute-list ] ]`. A sequence with a +// missing closing `]` is a parse error -- a single `[` is not a valid attribute-specifier, +// so the parser rejects it rather than silently accepting the malformed input. +[[deprecated]] int ok(void); +[[deprecated] int bad(void);