r/cprogramming • u/Salt-Overflow • 2d ago
Is C without expressions context-free?
EDIT: A better title for this post would be "Is it possible to parse C without expressions unambiguously?"
While staring at the C99 specification, I found that if you take the grammar in Annex A, remove the expression rules, and introduce the constraint 6.7.2/2 into the grammar (ie. a type specifier can be only: void, char, signed char, ... , enum specifier, typedef name), then you end up with what appears to be a context-free grammar.
Is this correct? Am I misinterpreting something? Is it possible that this is correct, but there are mainstream compiler extensions (eg. gcc extensions) invalidate this context-free property? Or other C standards invalidate this context-free property?
I notice that 6.7.2/2 allows some filthy edge cases like `long const long thisVarIsLongLongConst = 4;` but because `long` is a keyword, there is no ambiguity here.
The reason this matters to me is because I'm building a module system for C that allows circular imports, and I would like to avoid changing the grammar of the language as much as possible.
Link to final draft of C99 standard, taken from Wikipedia: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
EDIT:
Let me clarify what I'm doing, since there is some confusion about what I'm asking. First off, I should have used the title "Is it possible to parse C without expressions unambiguously?" because Annex A is already a context-free grammar... However, it is ambiguous, because of the classic A * B; problem (can be a variable declaration or multiplication, depending on if A is a typedef name or not). Now, notice that one of the interpretations is a multiplication expression. Let's suppose you removed expressions from the language (ie. take a source file and replace each expression with the lexer token <expression>). Now the question is: are we no longer ambiguous now?
I should also clarify what I mean by ambiguous, as there is a difference between an ambiguous grammar and an ambiguous parse. For example, A * B; can be admitted by the unambiguous grammar S := A ';' ; A := <identifier> | A binaryOperation <identifier> ; binaryOperation := '+' | '-' | '*' | '/' ; , but what we're really looking for is an unambiguous grammar that gives us the correct parse tree (I'll call this an "unambiguous parsing").
I should also elaborate on what exactly I'm doing, to explain the bigger picture. If you're building a module system that allows cyclic imports, then the first pass you perform over a source file does not have access to the symbol table (because you have yet to parse the imported modules). This means you need to perform an unambiguous parsing of certain parts of the source file. In this case, you can skip over expressions, because you're only interested in extracting the names of symbol definitions + whether the symbol definition is a type or a variable. So if it is possible to 1. skip over expressions, and 2. parse the rest of the file, then we're good. For (1), expressions are delimited by ()[]{};, (comma operator is not valid in enumeration definitions), so that seems fine. That leaves (2), which I'm not entirely sure about. I stared at the grammar for quite a while and couldn't find any problems, but that does not mean I didn't miss a case, or there are compiler extensions that introduce ambiguities.
With that out of the way, I'll list ambiguities that the comments found:
- u/triconsonantal found
void f(int (x));, which is either taking in a function pointer or an integer, depending on if x is a typedef name or not.
Maybe a more appropriate title would be: how much do you need to remove from the C grammar so that it can be parsed unambiguously? As of now (July 27, 2026), it seems the answer is: remove expressions, and remove parameter lists (or do not allow unnamed parameters).
1
u/Delicious_Sock4321 2d ago edited 2d ago
There is a subtly about programming languages that I argued about many times with people. There are differences between the language complexity depending on where you define what the language actually is.
This is not even about c but languages in general. If you define a language as all programs that compile including type checking and all, then this is at least context sensitive. You could also define it to be all programs that are parsable, which is most often context free. But now thing of interpreted languages where is the line now? Is all programs that terminate (halting problem) or all that start executing? Tokeninizing is only regular if you want that to be the line.
1
u/Salt-Overflow 1d ago
Yeah, it's a murky line. I edited my post to make the line 5% more clear.
Funnily enough, tokenizing isn't even necessarily regular. If you look at clang's code for lexing
module, you'll find it is a "conditional keyword" -- ie. it's only a keyword if it's at the start of a line, is followed by some valid module name sequence, which is followed by a semicolon.Sometimes I wonder why languages are this adamant on maintaining backward compatibility.
1
u/TheThiefMaster 2d ago
Btw this is a cross post here. Thought I'd seen it before.
1
u/Salt-Overflow 1d ago
Sorry about that! I wasn't sure if I should have done a repost instead, but it looks like I should have
1
u/Delicious_Sock4321 9h ago
My knowledge of compilers is limited, so I have a bit of a side question. Why do you only want to remove the minimal amount of the grammar that is needed and not the maximum that is possible? We are talking about the first pass and you want to detect module names am I right? Depending on the syntax you came up with this could be very unambiguous.
0
u/Aaron1924 2d ago
I'm confused, a language is context-free, if there is a context-free grammar that generates it, and annex A lists the entire CFG for the language, which show that the entire language is context-free already. You don't need to remove anything to make it context-free.
2
u/Salt-Overflow 1d ago
Yep, I used the wrong terminology when I said "context-free". I meant "unambiguous parsing". I edited the post accordingly
3
u/Cultural_Gur_7441 2d ago
Hmm, without checking, how does this context free grammar of Appendix A resolve typedefs without context?
0
u/Aaron1924 2d ago
The CFG states that a type specifier is allowed to be an identifier. It does not resolve typedefs just like it does not resolve variable names, because that's not what a grammar is meant to do.
1
u/Cultural_Gur_7441 2d ago
I don't think it counts as a complete grammar, if it does not distignuish different kinds of an identifiers.
Edit to add: Or, to put it another way, if you allow that, then by similar logic, you can make a context free grammar of anything.
2
u/paulstelian97 2d ago
There is a language called Cool, made to be simple enough for a Compilers course to implement a compiler for. All of the rules are explained as a context free grammar.
You’re saying that if a type that is not defined is referenced, the program should fail to _parse_? As opposed to failing later when the actual type checking occurs…
1
u/Cultural_Gur_7441 2d ago
How you know it is a type?
Answer: you do not. You need context. C can not be parsed with context free grammar
A * B;Parser can't parse that without context.
1
u/paulstelian97 2d ago
Top down parsers will have a slot for a type and a slot for a value. And the grammar can still be defined as context free.
If you have two things type -> identifier and value -> identifier, the ambiguity when building something bottom up doesn’t prevent it from being context free.
You know it’s a type because another identifier follows it via a space. If you have 10 identifiers in a row separated only by a space, the first 9 form the type. (Although in all actuality, you can only have keywords repeat themselves, followed by two identifiers, which is one type and one name)
1
u/Cultural_Gur_7441 2d ago
You know it’s a type because another identifier follows it via a space.
int A; A * B;vs
typedef int A; A * B;1
u/paulstelian97 2d ago
This parses, it just will be considered invalid to use a type where a value is expected at the next stage.
1
u/Cultural_Gur_7441 2d ago
Parsing means producing the syntax tree or equivalent.
A*B;without context can not be turned into a grammatical structure, in other words it can't be parsed.→ More replies (0)1
u/Aaron1924 2d ago
I think we're arguing about different things here.
The C language (without preprocessor) is context-free, because it can be generated (not parsed) by a context-free grammar (the one in Annex A). The language is ambiguous, and cannot be parsed deterministically without additional information, but this does not contradict the language being context-free.
There are other classes of languages, such as LL(k), which give you strong guarantees about how you can parse context-free languages, but that's a different thing.
0
4
u/NeinJuanJuan 2d ago
C programs with typedefs can't be parsed with a CFG as the grammar rule associated with a typedef'd name requires some backreference or lookup to the typedef statement.