the perils of parsing type inference declarations in c

Lobsters Hottest News

Summary

This blog post explores parsing ambiguities in C23 involving `auto` as a type inference specifier or storage-class specifier, showing how GCC and Clang disagree on parsing declarations like `auto x = 67;` when `x` is a typedef, and how attributes complicate the situation.

<p><a href="https://lobste.rs/s/ypgw9x/perils_parsing_type_inference">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 07/25/26, 08:01 AM

# the perils of parsing type inference declarations in c Source: [https://sebsite.pw/w/20260725-auto.html](https://sebsite.pw/w/20260725-auto.html) here's something fun\. say that`x`was declared in an outer scope as atypedef: ``` typedef int x; ``` now take the following code in a function body: ``` auto x y = 67; ``` this declares an automatic variable named`y`with type`x`\. both gcc and clang parse this correctly\. cherish this moment: it will be the last time in this blog post that gcc and clang agree on how to parse a declaration\. let's change the declaration to look like this instead: ``` auto x = 67; ``` this should \(presumably\) declare a variable named`x`with an inferred type; the new binding should shadow thetypedef\. and this is exactly what clang does\. gcc, on the other hand, ``` <source>: In function 'main': <source>:4:12: error: expected identifier or '(' before '=' token 5 | auto x = 67; | ^ ``` the problem is that`auto`can be either a storage\-class specifier*or*a stand\-in for a type specifier, depending on context\. here, gcc parses`x`as atypedef, and so treats`auto`as a storage\-class specifier, and therefore reports a syntax error\. clang looks ahead before deciding how to treat`x`, so it's able to parse both declarations\. so, what's the intended behavior? is this a bug in gcc? well\.\.\. it's tough to say\. as far as i can tell, the standard doesn't explicitly disambiguate here\. funny enough, this same ambiguity arises in ansi c, since declarations which omit type specifiers are implicitly given type`int`, but ansi c explicitly disambiguates: > If the \[typedef\] identifier is redeclared in an inner scope or is declared as a member of a structure or union in the same or an inner scope, the type specifiers shall not be omitted in the inner declaration\. so per ansi c rules, gcc is correct to error out here\. but because i can't find any explicit disambiguation in the c23 standard, my interpretation is that clang's behavior is correct, since it fits in the grammar\. and that's really funny, because it makes parsing really difficult\. it might seem straightforward to just look ahead a token, but let's try throwing in some attributes: ``` auto x [[asdf]] [[ghjk]] y = 67; ``` this should declare`y`with type`x`, where`x`is given the attributes`\[\[asdf\]\]`and`\[\[ghjk\]\]`\. this time, only gcc parses it successfully\! clang gives this error message: ``` <source>:4:10: error: declaration of variable 'x' with deduced type 'auto' requires an initializer 5 | auto x [[asdf]] [[ghjk]] y = 67; | ^ <source>:4:20: error: expected ';' at end of declaration 5 | auto x [[asdf]] [[ghjk]] y = 67; | ^ | ; ``` clang is attempting to apply the attributes to a binding named`x`, and erroring out when it sees`y`after the attributes\. let's get rid of`y`and see what happens now: ``` auto x [[asdf]] [[ghjk]] = 67; ``` now it's back to gcc erroring out: ``` <source>: In function 'main': <source>:4:21: error: expected identifier or '(' before '=' token 5 | auto x [[asdf]] [[ghjk]] = 67; | ``` so gcc sees the attributes and tries to bind it to the type`x`\. neither gcc nor clang backtrack later if their assumption turns out to be incorrect\. once again, the standard never explicitly disambiguates, so as far as i can tell, the "correct" behavior should be to handle both\. so to parse this correctly, all attributes after the identifier need to be parsed, then another token needs to be read to decide whether or not the identifier is a type specifier, and then the attributes need to be retroactively applied to either the type or the binding depending on how the identifier is interpreted\. it gets even better though: clang supports array, function, and pointer declarators with type inference\. the standard says that this is implementation\-defined: > Implementations can accept a direct declarator that is not of the form``` identifier attribute-specifier-sequenceopt ``` optionally enclosed in pairs of parentheses; if a direct declarator of a different form is accepted, the behavior is implementation\-defined\. it's a bit weird, because "direct declarator" excludes unparenthesized pointer declarators, which was probably an unintentional oversight\. but either way, because clang chooses to support this, i'm pretty sure the following declaration is completely ambiguous: ``` typedef int x, y; int main() { auto x (y) = 67; } ``` this is either an automatic variable`y`with type`x`, or a variable named`x`whose type is a function accepting`y`and returning an inferred type \(a constraint violation\)\. clang chooses to parse it as the latter: ``` <source>:4:5: error: 'auto' not allowed in function return type 5 | auto x (y) = 67; | ^~~~ <source>:4:10: error: illegal initializer (only variables can be initialized) 5 | auto x (y) = 67; | ^ ``` although the standard says the behavior is implementation\-defined, i do think it should explicitly disambiguate here, because this is an issue with syntax, not semantics\. oh oh oh also: prior to the addition of type inference to standard c, gcc and clang both supported the extension`\_\_auto\_type`, which has exactly the same semantics as c23's`auto`\. \.\.\.is what i previously thought, before going down this rabbit hole\. since`\_\_auto\_type`isn't a storage\-class specifier, it's not exactly equivalent to`auto`, because the identifier will never be treated as a type specifier\. and that makes sense, but it got me thinking: does`\_\_auto\_type`have the same scope semantics as c23 type inference? so, ok\. normally, declarations are inserted into the scope after the declarator is parsed, but before the initializer \(if present\)\. this doesn't really work as\-is for type inference \(or`constexpr`\)\. so to accomodate this, c23 introduces the concept of "underspecified" declarations\. that is, the declaration is inserted into the scope as normal after the declarator, but it doesn't have a type, so attempting to use it anywhere in the initializer is a constraint violation: ``` // ok: x is initialized to a poison value int x = x; // error: x is underspecified auto x = x; constexpr int x = x; ``` but since underspecified declarations are a new thing, does`\_\_auto\_type`have the same semantics? let's try it out: ``` int x; int main() { __auto_type x = x; } ``` this compiles with gcc, since gcc doesn't insert the new declaration into the scope until*after*the initializer is parsed\. clang, on the other hand, errors out, since it uses the same semantics as c23 underspecified declarations\! interestingly, this divergence in behavior has existed for as long as`\_\_auto\_type`has existed in both compilers\. that is, clang has always errored out here, even before type inference was standardized\. anyway, to summarize, correctly parsing type inference declarations requires: - only deciding whether atypedefidentifier is a type specifier after looking ahead for a binding, - allowing the identifier to be followed by attributes, and retroactively applying the attributes to either the type or the binding depending on how the identifier is interpreted, - and who the hell knows for function declarators and`\_\_auto\_type`\.

Similar Articles

Discussion about C array type semantics

Lobsters Hottest

The article explains the confusing behavior of C array types, including their decay to pointers, exceptions like sizeof and function parameters, and compares it to function types, suggesting a mental model where arrays and pointers are strictly separated.

Parse, Don't Validate – In a Language That Doesn't Want You To

Hacker News Top

A blog post exploring the 'parse, don't validate' principle in TypeScript, showing how to use branded types to preserve type information after parsing, despite TypeScript's structural typing making this less idiomatic than in languages like Elm or Haskell.

On C extensions, portability, and alternative compilers

Lobsters Hottest

This article discusses the practical challenges of writing portable C code due to reliance on non-standard compiler extensions and glibc's conditional headers, illustrating with examples from building a C compiler.

Getting silly with C, part &((int*)-8)[3]

Lobsters Hottest

A humorous educational article covering C programming fundamentals such as forward declarations, operator precedence, unconditional jumps, and basic arithmetic with intentionally silly code examples.

No way to parse integers in C (2022)

Hacker News Top

The article criticizes C standard library functions for parsing integers (atol, strtol, strtoul, sscanf), explaining why most are broken and only strtol can be used correctly with careful error handling.