Programming Languages · Language Design
The Dangling Else and How Grammars Resolve Ambiguity
Why an if-statement grammar is ambiguous, how shift-reduce parsers settle the conflict by default, and what brace-less and indentation-based designs cost instead.
The grammar for conditional statements is one of the first things a language designer writes, and it is ambiguous in almost every form that omits an explicit terminator. The ambiguity is small, well understood, and resolved by convention in every language that has it. Understanding the resolution is a compact lesson in how parsing decisions get made: not by the grammar alone, but by the parser’s conflict policy, the language’s specification, and the tooling around it.
The grammar that cannot decide
Write the obvious production rules for a language with optional else:
stmt -> 'if' expr 'then' stmt
| 'if' expr 'then' stmt 'else' stmt
| other
Now parse if a then if b then s1 else s2. Two derivations exist. In the first, the outer if takes the else: if a then (if b then s1) else s2. In the second, the inner one does: if a then (if b then s1 else s2). Both are valid parse trees for the same token string, and they mean different things. If a is false, the first form runs s2 and the second does nothing.
This is not a flaw in the particular rules above. Any grammar that makes else optional and allows a statement in the then branch will admit the string with two derivations. The ambiguity is structural, not a mistake in transcription.
Shift or reduce
A bottom-up parser such as the one Bison generates reaches the ambiguity as a concrete decision. After reading if a then if b then s1 with else on the lookahead, the parser can either reduce if b then s1 to a statement, which leaves the else for the outer if, or shift the else onto the inner one. That is a shift-reduce conflict, and Bison reports it.
Bison’s default resolution is to shift. The manual states the rule plainly: when a shift-reduce conflict cannot be resolved by precedence, the parser shifts, and a warning is emitted. Shifting attaches the else to the innermost open if, because shifting means continuing the production that is currently on top of the stack. The nearest-if rule is therefore not written anywhere in most grammars; it is the consequence of a default conflict policy.
The conflict can also be resolved declaratively, by assigning precedence to the then and else tokens so that shifting else outranks reducing the inner conditional. Either way the resulting language is the same, and the grammar remains ambiguous as written. The ambiguity is resolved in the parser’s conflict table, not in the grammar text. That is worth knowing because the same grammar fed to a different parser generator, one that refuses conflicts rather than resolving them, is rejected outright. ANTLR, for instance, resolves an ambiguity by taking the alternative that appears first in the grammar, so the order in which the alternatives are written decides the meaning.
A recursive-descent parser has no conflict table and must encode the decision in code. The usual shape is a function that parses a statement and, after the then branch, checks whether the next token is else and consumes it. Because the recursive call for the inner statement runs to completion before the check for else happens, the inner call consumes the else first. Nearest-if again, this time from call order.
The rule as a specification sentence
Most language specifications state the outcome directly rather than deriving it. The C standard’s grammar for the selection statement contains the same ambiguity, and the standard resolves it with a sentence: an else is associated with the nearest preceding if that lacks one. Java, C++, and C# carry equivalent wording. Once stated that way, the ambiguity is a documentation matter, and implementations are free to resolve it however they like as long as they agree.
The sentence has a consequence beyond the conditional. Because else binds to the nearest unmatched if, a macro that expands to more than one statement breaks when used in a brace-less then:
#define LOG(x) printf("%s\n", x); fflush(stdout)
if (ready)
LOG("start");
else
LOG("wait");
The expansion is if (ready) printf(...); fflush(stdout); else ..., and the else now has no if to attach to. The compiler rejects the program. The conventional fix, wrapping the macro body in do { ... } while (0), works precisely because the do statement is a single statement and the trailing while (0) swallows the semicolon that a caller would otherwise supply. The idiom exists because the grammar has no rule for a statement sequence in a branch position.
Terminators, braces, and columns
The alternative designs all remove the ambiguity by making the extent of a branch explicit, and each pays a different price.
Algol 68 and several later languages close conditionals with a keyword, so if ... then ... else ... fi has no unmatched-if state. Lua uses end for every block. Ruby uses end as well. The cost is one more token per block, and the benefit is that the grammar is unambiguous and error messages point at a missing terminator rather than at a mysteriously misassociated else.
C and its descendants require braces only when a branch contains more than one statement, which is exactly the case where the ambiguity would otherwise be visible. In practice the brace-less form survives because the nearest-if rule matches what most programmers intend. Style guides that mandate braces on every branch are not protecting against the ambiguity, which the compiler resolves; they are protecting against the macro case and against later edits that add a second statement to a branch and forget the braces.
Python removes the token entirely and uses indentation. The grammar is defined over INDENT and DEDENT tokens produced by the tokenizer from leading whitespace, so a block’s extent is a property of the layout rather than of a delimiter. The else clause is then unambiguous because the if body has an explicit end at the dedent. The trade is that whitespace becomes semantically significant, that mixed tabs and spaces are a lexical error rather than a formatting preference, and that a block cannot be reformatted without changing the token stream. Python’s specification also notes the dangling-else problem does not arise, because the grammar’s block structure is not optional.
Go sidesteps the question by requiring braces and by requiring the opening brace on the same line as the condition, a rule that follows from the scanner’s line-terminated semicolon insertion. Since a brace-less branch does not exist, no ambiguity exists. The formatter enforces the layout, so the constraint never surfaces as a decision.
Why the ambiguity survives
A language designer could remove the ambiguity and keep the syntax by giving then a precedence that forces reduction, which is what a precedence declaration does. That changes nothing observable, because the language specification already fixed the meaning. The ambiguity is harmless in the sense that every implementation resolves it the same way.
It is not harmless in two situations. The first is grammar tooling: an ambiguous grammar cannot be used to generate a parser in a framework that rejects conflicts, and the error message names a rule the author may not have thought about. The second is language extension. Adding a new statement form that can begin with a token also usable as a continuation, or adding an optional clause after a conditional, reopens the conflict in a new place. Languages that permit optional trailing clauses, such as a then without else plus a subsequent catch, must ensure the new clause cannot be absorbed by an inner statement.
The general lesson recurs in Automatic Semicolon Insertion and Restricted Productions: a construct that can be read two ways is settled by a rule outside the grammar text, and the rule’s location determines whether a mistake is a compile error or a silent change of meaning. Here the rule is a conflict policy; there the rule is a parse-time repair. Both are conventions layered over a grammar that does not by itself pick a single tree.
Nested switches and the same shape
The dangling-else pattern is not unique to conditionals. A switch whose cases do not require break has an analogous nesting problem: a switch inside a case, with a break that could belong to either, resolves by the same nearest-enclosing rule. C’s break exits the innermost enclosing switch or loop, and the specification states it as such, so the meaning is defined even though the source is easy to misread. Languages that made break mandatory or replaced fallthrough with explicit continue-like keywords removed the reading difficulty at the cost of verbosity.
The pattern to recognize is a grammar production with an optional trailing part, nested inside itself. Wherever that shape appears, the language needs a rule for which enclosing construct the trailing part belongs to, and the rule will be either a parser conflict policy, a specification sentence, or a syntactic delimiter. Choosing one of those three is a design decision with visible consequences for error messages, macro hygiene, and tooling, even though the accepted language is identical in all three cases.
The third kind of ambiguity, where two operators compete for the same operand, is settled by a different table. Operator Precedence Tables and Why They Are a Design Artifact describes how numeric levels replace grammar structure, and why those levels are as constrained as the conflict policy described here.
References
- Bison — GNU parser generator manual, Conflict Management — GNU Project
- Compilers: Principles, Techniques, and Tools — Aho, Lam, Sethi, Ullman; Addison-Wesley
- The Python Language Reference — Compound statements — Python Software Foundation