3 ms·
C is notoriously painful to parse with a parser generator because it's not context free --- consider this statement: foo * bar; It can't be parsed until y
by david-given 11y ago
C is notoriously painful to parse with a parser generator because it's not context free --- consider this statement:
foo * bar;
It can't be parsed until you know whether foo is a type or a variable --- indeed, depending on the implementation, it can't even be tokenised correctly. So you need hooks from the symbol table back into the tokeniser/parser. I'm not at all surprised that the author's using a hand-written parser, as it reduces the problem space considerably.
(There's a really good article which I've been trying to find which goes into details about all the weird parsing edge cases due to the rules about exactly when a word goes into the symbol table which I've been completely unable to find...)
- andrewchambers 11y agoHere is a good one http://eli.thegreenplace.net/2012/07/05/how-clang-handles-the-type-variable-name-ambiguity-of-cc http://eli.thegreenplace.net/2012/07/05/how-clang-handles-th...
- yepguy 11y agoAs far as I know C is context-free. If C's grammar is ambiguous, that just makes it an ambiguous context-free grammar [1]. [1]: https://en.wikipedia.org/wiki/Ambiguous_grammar https://en.wikipedia.org/wiki/Ambiguous_grammar
- kazinator 11y agoThe context-free terminology doesn't apply to C because the ambiguity rests in not knowing what is the lexical category of foo in "foo * bar". That information comes from semantics. A language whose semantics you have to understand to understand its syntax is outside of the entire formal language realm inside which we formally recognize a "context-free" category. That formal language realm is purely syntactic: the terminal symbols are just "dumb" and stand for themselves. They are subject to rules which indicate how those symbols are combined. C is "quasi context free" in that if we resolve what the raw tokens mean (which one is a type and which one isn't), then the resulting language is context-free. In fact stronger than context free: amenable not only to LALR(1) parsing approaches but to recursive descent, with a suitable refactoring of the left-recursive rules given in the standard.
- yepguy 11y agoYour post doesn't really agree with what I was taught. > the ambiguity rests in not knowing what is the lexical category of foo in "foo * bar". Right, so there are 2 possible parse trees. Being able to disambiguate them with more information doesn't mean it's not context-free. Given just a CFG, you were still able to determine that you have a syntactically valid C program. > A language whose semantics you have to understand to understand its syntax is outside of the entire formal language realm inside which we formally recognize a "context-free" category. A formal language is just the set of strings that belong to it. That's it. An absurd example might be "the set of strings corresponding to valid C programs that do not terminate". To recognize such a language, you would not only have to understand the semantics of C, you would even need to solve the halting problem, but that doesn't make it "outside of the entire formal language realm".
- kazinator 11y ago> Right, so there are 2 possible parse trees. Not with the same symbols, though. > A formal language is just the set of strings that belong to it. That's it. That is correct, and in that framework, we cannot reason about the meaning of "foo"; that if it's a type apply this rule otherwise that rule. If we do that reasoning, what we're really doing is replacing "foo" with one of two other symbols, and then parsing those; then we have different strings (so of course the parse trees cannot be the same, no matter what).
- yepguy 11y agoWe don't need to decide to apply this rule sometimes but other times that rule. When we come to an ambiguous string, we apply both rules nondeterministically. Here's a pseudo-grammar that I think describes both possibilities. Statement -> Type Op Var ";" Statement -> Var Op Var ";" Op -> "*" I think the disconnect here is that most lexers would insist on feeding a different token to the parser depending on whether "foo" is a Type or a Var, and choosing one or the other means the lexer might feed it bad information. The pragmatic way to implement this in a real compiler is to add some logic to the lexer so that it's working with more information than just the syntax. Theoretically though, all that really matters is that both possibilities are syntactically valid. So another way to implement it would be to pass on both possibilities, letting the parser eliminate the one that doesn't compile.
- JoeAltmaier 11y agoBut that hook is trivial. Scanner returns Identifier or Type as appropriate. Not really painful at all.
- kazinator 11y agoC is completely easy to parse, because it reduces to a LALR(1) parseable context-free language after the semantic mapping of input symbols into abstract lexical categories. In fact, it can be parsed by recursive descent with a suitable factoring, so that it is almost certainly LL(k), possibly with k = 1. Pardon me, k == 1. :) That mapping requires semantic information (type declarations), but that information is something the compiler must maintain anyway so that it can type check the language. Yes, if we simply treat "foo" as an IDENTIFIER token and feed that to the parser, thing swill be difficult. But we don't; what we return to the parser is a TYPESPEC token, based on peering into the current parse-time lexical environment where we resolve "foo" to a typedef name. The parser then quite easily goes through the state transitions corresponding to its declaration rule. Sure, a parser which maintains parse-time lexical environments which are also visible to the lexer is slightly harder to write than one which doesn't have such stuff (and just returns an unadorned AST which you can walk again to build that info). But if the year is 1973, you would likely do that anyway even if the language doesn't require it. Why? Because building the compile-time environment tree reduces the number of passes: from a single pass through the raw code, you already have the syntax tree with type info (and possibly type info that has even been checked).
- dbpokorny 11y ago> you need hooks from the symbol table back into the tokeniser/parser not necessary. you can add a bit of information to each identifier token: is this the name of a type or not? you have to split up one of the parser passes, so if you had a two pass "tokenizer" and "LR parser" compiler now you have a tokenizer pass followed by two LR parser passes. The first LR pass is "last mile tokenization". I guess another word would be a (very short) cascade tokenizer/parser. The intermediate pass adds a piece of metadata to each token marking it as the name of a type or not (this makes the language actually context free instead of just nearly context free). If you do this, then the second pass doesn't see this as two identifiers at all because it knows which identifiers refer to types and which do not. It's a different application (computing scope data instead of type data) but I've implemented the basic concept of annotating token data with the result of an intermediate parsing stage here...check out line 318 here: https://github.com/dbpokorny/autoclave/blob/master/tree.js https://github.com/dbpokorny/autoclave/blob/master/tree.js