Grammars and BNF

Every programming language needs a precise way to say which strings of characters are legal programs. English descriptions like "an expression is a term followed optionally by a plus sign and another expression" are ambiguous. They are also impossible to feed to a machine. Compiler writers needed a notation that is both human-readable and mechanically checkable. That notation is the context-free grammar, usually written in Backus-Naur Form (BNF) or its extended cousin EBNF. This idea sits at the foundation of every parser. It underlies every language reference manual. It underlies every tool that turns source text into a structured tree a compiler can analyze. When you read the reference manual for Python, JSON, SQL, or a regular expression engine, you are reading a grammar. When a compiler prints "syntax error: unexpected token", it is because your program failed to match any rule of that grammar. Understanding grammars means understanding the contract between what a programmer writes and what a compiler is willing to accept. The big picture is this: a grammar is a small, finite set of rewriting rules. It generates an infinite set of valid strings. It separates the question of "what does this program look like" (syntax) from "what does it mean" (semantics). This is a separation you have already met as Syntax vs Semantics. Grammars give syntax a formal, testable definition. That formal definition is what makes automated parsing possible at all. This article builds context-free grammars from first principles. It shows exactly how BNF and EBNF notate them. It walks through derivations and parse trees step by step. It is honest about where the formalism runs into trouble — ambiguity, left recursion, and the gap between "the string is well-formed" and "the string means something". It closes by connecting grammars to the machinery — parsers — that will consume them. It also foreshadows how expressions and statements are themselves just grammar rules in disguise.

Key takeaways
  • A context-free grammar (CFG) is a set of rules that generate exactly the strings belonging to a language.
  • BNF writes rules as `
  • A grammar defines syntax precisely, separating "is this well-formed" from "what does it mean" (Syntax vs Semantics).
  • Derivations rewrite a start symbol step by step into a string; parse trees record that process as a tree structure.
  • Grammars can be ambiguous (one string, multiple parse trees) — a serious defect that language designers must eliminate or resolve by convention.
  • Grammars are the direct input to parser generators and the blueprint for how Expressions and Statements are structured.
The core insight

A grammar is a finite recipe of rewrite rules that precisely and mechanically defines an infinite set of valid programs.

The idea

A context-free grammar is a 4-tuple G=(N,Σ,P,S)G = (N, \Sigma, P, S): a finite set of nonterminal symbols NN (syntactic categories like "expression" or "statement"), a finite set of terminal symbols Σ\Sigma (the actual tokens of the language, like `+`, `if`, or an identifier), a finite set of production rules PP, and a designated start symbol S∈NS \in N. Each production has the form A→αA \to \alpha, where AA is a single nonterminal and α\alpha is any string of terminals and nonterminals. The word "context-free" refers to this restriction: the left-hand side is always a lone nonterminal, so a rule can be applied regardless of what surrounds it — no contextual conditions are checked.

Backus-Naur Form is simply a standardized textual syntax for writing down such rules. A nonterminal is written `` (or in modern usage often just `Name`), the rule arrow is written `::=`, and alternative right-hand sides are separated by `|`. For example: ``` ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ::= | ```

EBNF (Extended BNF) adds regular-expression-style shorthand on top of plain BNF, without changing the underlying formal power: `{ X }` or `X` for zero-or-more repetitions, `X+` for one-or-more, `[ X ]` or `X?` for optional, and parentheses for grouping alternatives. EBNF is more convenient to write and read, but every EBNF grammar can be mechanically expanded into an equivalent plain BNF grammar by introducing auxiliary nonterminals for each repetition or option — so EBNF adds no generative power, only notational sugar. The language generated by a grammar, L(G)L(G), is the set of all terminal strings reachable from SS by repeatedly applying productions — a process called <strong>derivation</strong>. Crucially, a grammar is a generative* device: it does not describe how to check a string, only how to produce one. Parsing is the inverse problem — given a string, find a derivation (if one exists) that produces it — and that inversion is what a parser algorithm computes.

How it works

Step 1 — Define the vocabulary. Decide the terminals (actual characters or tokens the lexer produces, such as `id`, `num`, `+`, `(`, `)`) and the nonterminals (syntactic categories such as `Expr`, `Term`, `Factor`).

Step 2 — Write productions for each nonterminal. Each nonterminal gets one or more alternative right-hand sides describing every shape that category can take.

Step 3 — Designate a start symbol. This is the nonterminal representing "a whole valid program" or, in a small example, "a whole valid expression".

Step 4 — Derive strings by rewriting. Starting from the start symbol, repeatedly replace one nonterminal in the current string with the right-hand side of one of its productions, until only terminals remain. This sequence of rewrites is a derivation; if at each step we always rewrite the leftmost nonterminal, it is a leftmost derivation.

Step 5 — Record structure with a parse tree. Each derivation corresponds to a tree: the root is the start symbol, and each internal node's children are the symbols on the right-hand side of the rule used to expand it. The leaves, read left to right, spell out the derived string. Here is a small, standard grammar for arithmetic expressions with `+`, ``, parentheses, and identifiers, written in BNF, designed to already encode precedence (multiplication binds tighter than addition) and left-associativity: ``` <expr> ::= <expr> "+" <term> | <term> <term> ::= <term> "" | ::= "(" ")" | "id" ``` The equivalent EBNF, using repetition instead of recursion, is more compact but describes the same language: ``` expr ::= term ( "+" term ) term ::= factor ( "" factor ) factor ::= "(" expr ")" | "id" ``` A general algorithm for producing a leftmost derivation of a target string (the essence of what a top-down parser automates) looks like this: ``` function DERIVE(grammar, target_string): frontier = [ grammar.start_symbol ] // current sentential form, as a list steps = [] while frontier contains a nonterminal: A = leftmost nonterminal in frontier choose a production A -> alpha // choice guided by lookahead at target_string replace A in frontier with the symbols of alpha steps.append(frontier.copy()) if frontier's terminal prefix does not match the corresponding prefix of target_string: backtrack: undo the last choice, try the next alternative for A if no alternatives remain: fail // string not in the language, or wrong path if frontier == target_string: return steps // a successful derivation else: return "not in L(grammar)" ``` This is a search procedure, not a magic trick: at each nonterminal there may be several productions to try, and a naive implementation may need to backtrack. Real parsers avoid this cost by restricting grammars to forms where the correct production can be chosen from a small amount of lookahead (the subject of predictive and LL/LR parsing), but the definition* of "valid string" never changes — it is always "some derivation from SS reaches it".

Figure
Parse tree for `id + id * id` under the precedence-respecting grammar
expr expr + term term factor id factor id * factor id Leaves read left-to-right: id + id * id — the "*" subtree nests inside the right operand of "+".

Complexity & correctness

Recognition complexity. For an arbitrary context-free grammar, deciding whether a string of length nn belongs to L(G)L(G) can be done in O(n3)O(n^3) time by the CYK algorithm (after converting the grammar to Chomsky Normal Form) or by the Earley parser, which is also O(n3)O(n^3) worst-case but O(n2)O(n^2) for unambiguous grammars and O(n)O(n) for the deterministic, bounded-lookahead grammars (like LL(1) or LR(1)) that real compilers use. The cubic bound comes from dynamic programming over all O(n2)O(n^2) substrings, each combined in O(n)O(n) ways at the split point. Practical language grammars are engineered to be LL(k) or LR(k) precisely so that parsing collapses to O(n)O(n), avoiding the general cubic cost entirely — this is a central reason language designers write grammars in restricted, "parser-friendly" shapes rather than arbitrary CFGs.

Space. Representing the grammar itself takes space proportional to the total length of all right-hand sides — a small constant relative to program size. Parse trees take O(n)O(n) space for a string of length nn, since a context-free derivation produces a tree with O(n)O(n) leaves (terminals) and at most a linear number of internal nodes for grammars without useless unit-rule chains.

Correctness — what "the grammar is correct" even means. A grammar GG is correct for an intended language LL if L(G)=LL(G) = L exactly: every string the language designer intends to be legal has some derivation, and no illegal string has any derivation. This is proved, not asserted, typically by structural induction on derivations:

  • Soundness (every derivable string is intended-legal): induct on the length of the derivation. The base case is a single production reaching only terminals; the inductive step shows that if all shorter derivations produce intended-legal substrings, substituting them via a valid production keeps the whole string intended-legal.
  • Completeness (every intended-legal string is derivable): induct on the structure of the intended string (e.g., "every arithmetic expression is a sum of products, each product a sequence of atoms") and exhibit the matching production at each level.

Ambiguity is a correctness defect, not just an inconvenience. A grammar is ambiguous if some string has two distinct parse trees (equivalently, two distinct leftmost derivations). This matters because a parse tree is what downstream semantic analysis is anchored to; two different trees for `a - b - c` could mean (a−b)−c(a-b)-c or a−(b−c)a-(b-c) — different values. The grammar ``` expr ::= expr "+" expr | expr "-" expr | "id" ``` is ambiguous: `id - id - id` has two derivations corresponding to the two possible groupings. The fix used in the earlier grammar — separate `expr`/`term`/`factor` layers with left recursion `expr ::= expr "+" term | term` — removes the ambiguity by forcing a single, canonical grouping (here, left-associative), and this can be proved by showing the layered grammar generates a unique leftmost derivation for every string, via induction on the number of operators.

Left recursion is a parsing hazard, not a generative one. The rule `expr ::= expr "+" term` is left-recursive and generates exactly the intended language, but a naive recursive-descent (top-down) parser following this rule literally would call `expr()` inside `expr()` before consuming any input, looping forever. This is purely an artifact of the parsing strategy, not a flaw in the language L(G)L(G) itself — it is routinely eliminated by rewriting left recursion into right recursion or iteration (as the EBNF version above does with `term ( "+" term )*`), a transformation provably preserving L(G)L(G).

Worked example

Trace a leftmost derivation of the string `id + id id` using the layered BNF grammar from above. ``` expr ::= expr "+" term | term term ::= term "" factor | factor factor ::= "(" expr ")" | "id" ```

StepSentential formRule applied
0`expr`start symbol
1`expr "+" term``expr ::= expr "+" term`
2`term "+" term``expr ::= term` (leftmost `expr`)
3`factor "+" term``term ::= factor`
4`id "+" term``factor ::= "id"`
5`id "+" term "*" factor``term ::= term "*" factor`
6`id "+" factor "*" factor``term ::= factor`
7`id "+" id "*" factor``factor ::= "id"`
8`id "+" id "*" id``factor ::= "id"`

The final sentential form matches the target string exactly, so the derivation succeeds and the string is confirmed in L(G)L(G). The parse tree built from these eight steps is exactly the tree drawn in the SVG figure above: `` binds tighter because `factor "" factor` is nested one level deeper than the top-level `+`, and this nesting is what a later semantic pass (evaluation, type checking) will walk over — a preview of how Expressions and Statements get their structure directly from grammar shape, not from any special-casing in the compiler. Contrast this with the ambiguous flat grammar `expr ::= expr "+" expr | expr "" expr | "id"` on the same string: it admits a derivation where `+` is applied last (grouping as `id + (id id)`, correct) and one where the tree structurally allows `*` to attach to the wrong operand under different bracketings for longer strings — the ambiguity becomes concrete once you try `id + id + id`, which yields two distinct trees, one grouping as (id+id)+id(id+id)+id and the other as id+(id+id)id+(id+id); both are valid parses under the flat grammar, and nothing in the grammar itself picks one over the other. This is precisely the defect the layered grammar was designed to eliminate.

Common pitfalls & traps

PitfallWhy it happensFix
Ambiguous grammarFlat rules like `E ::= E op E \id` allow multiple groupings for the same stringLayer by precedence (`expr`/`term`/`factor`) so each precedence level has one production shape
Left recursion breaks naive recursive descent`E ::= E + T` calls itself before consuming inputRewrite to right recursion or EBNF repetition: `E ::= T ("+" T)*`
Confusing "generates" with "recognizes"Beginners think a grammar is an algorithmA grammar defines a set of strings; a separate parsing algorithm decides membership
Treating EBNF `*`/`+`/`?` as adding powerThey look like new capabilityThey are sugar for auxiliary nonterminals; the underlying language class is unchanged
Ignoring operator associativity in the grammar`E ::= T + E` (right recursion) silently makes `+` right-associativeChoose left- or right-recursive form to match the intended associativity, and check it explicitly
Mixing lexical and syntactic concernsTrying to make the grammar responsible for whitespace/comments/token boundariesDelegate that to a separate lexical grammar (regular, tokenizing) layer before the CFG

Pioneers & history

Origins · 1 minJohn Backus and how this was discovered
JB
John Backus
1924-2007 · IBM Research; led the team that designed FORTRAN and later co-designed ALGOL 60's syntax notation

Backus proposed the metalinguistic notation used to describe ALGOL 60's syntax at a 1959 conference; Peter Naur, editor of the ALGOL 60 report, refined and standardized it for the final language report. The two names were later joined into "Backus-Naur Form" by Donald Knuth, cementing the notation as the standard way to specify programming-language syntax.

History
1956
— Noam Chomsky formalizes the hierarchy of formal grammars, including context-free grammars, in the study of natural language.
1959
— John Backus introduces a metalinguistic formula notation to describe ALGOL 58/60 syntax at a UNESCO conference.
1960-1963
— Peter Naur edits and refines the notation for the ALGOL 60 report; Donald Knuth later names it Backus-Naur Form.
1965
— Donald Knuth defines LR(k) parsing, showing which context-free grammars admit efficient, unambiguous, deterministic parsing.
1970
s — Niklaus Wirth popularizes Extended BNF conventions (repetition and optional operators) in language reports for Pascal and successors, later formalized as ISO/IEC 14977.

Where this matters

Every major language specification — the ISO C and C++ standards, the Java Language Specification, the Python reference grammar, the ECMAScript specification — publishes its syntax as an (E)BNF grammar, and that grammar is the ground truth compiler and interpreter authors implement against. Parser generator tools such as Yacc, Bison, ANTLR, and Menhir take a BNF/EBNF-style grammar as literal input and automatically produce a working parser, turning the formalism directly into executable infrastructure. Data-interchange formats like JSON, configuration languages like YAML and TOML, and query languages like SQL all ship an official grammar so that independent implementations can agree on exactly what counts as valid input. Beyond programming languages, the same formalism underlies markup validation (XML DTDs and schemas), network protocol message formats (RFC grammars written in ABNF), and even some natural-language processing pipelines that use context-free grammars as an approximate model of sentence structure. Understanding grammars is the direct prerequisite to understanding how parsers, the next layer of any compiler front end, actually work.

Ask the tutor: Can you show me why the layered `expr`/`term`/`factor` grammar is provably unambiguous while the flat `expr ::= expr op expr | id` grammar is not?

What is the difference between a grammar and a parser?

A grammar is a declarative specification: a finite set of rewrite rules defining which strings belong to a language, with no notion of "algorithm" attached. A parser is a procedure that takes an arbitrary string and decides whether it belongs to that language, and if so, reconstructs a parse tree witnessing that membership. The grammar answers "what is legal"; the parser answers "is this particular input legal, and if so, what is its structure". You can write a grammar without ever writing a parser (as language specifications often do), but every parser is built to implement some specific grammar.

Why do compiler writers avoid ambiguous grammars if the ambiguity can just be resolved by convention?

Because an unresolved ambiguity is invisible until it silently causes different tools, versions, or implementations to disagree on what a program means. Resolving ambiguity "by convention" (e.g., "the parser always picks the first matching rule") makes the actual language depend on implementation details never written into the formal grammar, which undermines the entire point of having a written specification. Restructuring the grammar to be inherently unambiguous — as with precedence layering — makes the intended meaning explicit and checkable, and lets parser-generator tools verify the property automatically rather than trusting an undocumented tie-break rule.

Is every context-free grammar suitable for building a fast, deterministic parser?

No. Arbitrary CFGs can require general algorithms like CYK or Earley parsing, which cost up to O(n3)O(n^3) time for a string of length nn, and some CFGs are inherently ambiguous with no possible rewrite that removes the ambiguity. Practical language grammars are deliberately engineered to fall into restricted subclasses — LL(k) for top-down predictive parsers, or LR(k) for bottom-up shift-reduce parsers — which guarantee a unique parse can be found in linear O(n)O(n) time using only a bounded amount of lookahead. Designing a grammar to be LL(1) or LR(1) is a real engineering constraint, not automatic.

How does BNF relate to regular expressions, since both use similar-looking operators?

Regular expressions describe regular languages, a strictly smaller class than context-free languages; they can express repetition and choice but cannot express arbitrary nested, matching structure (like balanced parentheses or nested blocks) because they have no mechanism for unbounded recursive self-reference. BNF's core mechanism — nonterminals that reference themselves or each other, as in `expr ::= expr "+" term` — is exactly what supplies that extra recursive power. EBNF borrows regex-like sugar (`*`, `+`, `?`) purely for local repetition and optionality, but the presence of recursive nonterminal references is what makes the overall formalism strictly more expressive than regular expressions.

Can a grammar describe semantics, like what an expression evaluates to?

No — a context-free grammar is purely a syntactic device; it says nothing about meaning, only about which strings are well-formed and what their structural (parse-tree) shape is. Meaning is assigned separately, typically by attaching semantic rules or evaluation functions to each production (a technique formalized as attribute grammars or denotational semantics), operating on the parse tree the grammar produces. This is the same syntax/semantics separation introduced in Syntax vs Semantics: the grammar answers "is `id + id * id` legal, and how does it group", while a separate semantic definition answers "what value does it compute".

Why do real language grammars separate lexical rules (tokens) from syntactic rules (the CFG)?

Splitting the problem into a lexical grammar (usually regular, handling whitespace, comments, identifiers, numeric literals, and keywords) followed by a context-free syntactic grammar over the resulting tokens keeps each stage simple and efficient. The lexical stage can be recognized in linear time by a finite automaton, and the syntactic stage then only has to reason about a much smaller, cleaner token stream rather than raw characters, avoiding an explosion of low-level rules cluttering the CFG. This two-level design is nearly universal in real compilers and directly sets up the token stream that grammars for Expressions and Statements operate over.

What is the practical difference between writing a grammar in BNF versus EBNF for a real specification?

Both define exactly the same class of languages, so the choice is purely about clarity and maintainability, not expressive power. EBNF's `{ }`, `[ ]`, and grouping let you write `statement_list ::= { statement }` instead of introducing an auxiliary recursive nonterminal, which keeps specifications shorter and closer to how humans naturally describe repeated or optional constructs. Most modern language references (Python, ECMAScript, ISO standards) use EBNF variants for exactly this readability benefit, while textbooks teaching parsing theory often revert to plain BNF because its uniform `A -> alpha` shape is easier to reason about mathematically.

Before this

Where this leads