Language Theory §
Language theory studies how source code is structured (tokenization, parsing) and how its meaning is checked (type systems). It is relevant whenever a language must be designed, implemented, or extended.
Authoritative sources
Formal grammars and the Chomsky hierarchy §
A formal grammar is a tuple of:
- : a finite set of non-terminals (symbols that can be expanded),
- : a finite set of terminals (symbols that appear in the final string),
- : a set of production rules (replace non-terminal with string ),
- : the start symbol.
The Chomsky hierarchy classifies grammars by the restrictiveness of their production rules:
| Class | Production form | Automaton | Example |
|---|---|---|---|
| Regular (Type-3) | A → a | aB (right-linear) | DFA / NFA | Tokenizers, regex |
| Context-free (Type-2) | A → α (α ∈ (V ∪ Σ)*) | Pushdown automaton | Arithmetic, statements, expressions |
| Context-sensitive (Type-1) | αAβ → αγβ, |γ| ≥ 1 | Linear-bounded automaton | Some natural-language fragments |
| Unrestricted (Type-0) | α → β (any form) | Turing machine | General computation |
Programming languages are almost entirely context-free: their syntax (expressions, statements, declarations) is generated by context-free grammars. Tokenization is a regular problem. The boundary between the two is what separates the lexer from the parser.
Details
Why the hierarchy matters for compilers: a regular grammar can be recognized by a finite-state machine (no memory beyond the current state), which is exactly what a tokenizer does. A context-free grammar requires a stack (unbounded memory), which is what a parser uses. This is why tokenization is “easier” than parsing: it needs less memory and simpler logic.
Example grammar (arithmetic):
E → E + T | T
T → T * F | F
F → ( E ) | id
Here , , . The grammar is context-free because every production has a single non-terminal on the left.
The broader decidability and complexity questions (“can we always decide if a string is in the language?”, “how fast?”) are studied in Theory of Computation.
Automata §
Each grammar class in the Chomsky hierarchy corresponds to a class of automata (abstract machines that recognize the language):
| Grammar class | Automaton | Memory | Parser strategy |
|---|---|---|---|
| Regular | DFA / NFA | None (state only) | Table-driven lexer (flex, lex) |
| Context-free | Pushdown automaton (PDA) | Stack | LL (top-down), LR (bottom-up), PEG |
| Context-sensitive | Linear-bounded automaton | Tape bounded by input | General backtracking |
| Unrestricted | Turing machine | Unbounded tape | Undecidable in general |
Details
DFA (Deterministic Finite Automaton): a set of states, a transition function , one start state, and a set of accept states. At each step, exactly one transition is possible. Tokenizers are DFAs: the current state encodes “I am inside a number”, “I am inside a string literal”, etc.
NFA (Nondeterministic Finite Automaton): allows multiple transitions from a state on the same symbol (or -transitions). NFAs are equivalent in power to DFAs but can be more compact. Tools like flex build an NFA from regexes, then determinize it into a DFA.
Pushdown Automaton (PDA): a finite automaton augmented with a stack. The stack allows it to match nested structures (parentheses, blocks). This is the theoretical model behind all context-free parsers. In practice, parsers do not run a PDA directly; they use table-driven algorithms (LL, LR) or recursive descent that simulate the PDA’s stack with the call stack or an explicit stack.
The mapping grammar class → automaton → parser strategy gives a unifying framework: the more memory the automaton needs, the more complex the parser algorithm.
Tokenizers §
A tokenizer (lexer) converts a character stream into a sequence of tokens: keywords (if, let), literals (42, "hello"), operators (+, =>), identifiers, and punctuation. It is implemented as a DFA (a regular-language recognizer).
Details
Lexical analysis is a regular language problem: the set of valid tokens is described by regular expressions. Tools like lex/flex generate C code from regex specifications. The DFA states track what kind of token is being read (number, identifier, string, comment, etc.).
Maximal munch: the tokenizer always takes the longest possible match. a++ → a, ++ (not a+, +). a->b → a, ->, b (not a-, >b).
Preprocessing: many languages (C, C++) have a preprocessor that handles #include, #define, and conditional compilation before tokenization.
Why tokenization is separate from parsing: the regular-language restriction means the lexer can be a simple, fast table-driven loop with no backtracking. The parser then works on a clean token stream, free from character-level concerns. This separation is a direct consequence of the Chomsky hierarchy: regular ⊂ context-free.
Parsers §
A parser converts a token sequence into an AST (Abstract Syntax Tree) using a context-free grammar. The main strategies are:
| Strategy | Direction | Grammar class | Lookahead | Notes |
|---|---|---|---|---|
| Recursive descent | Top-down | LL(1) (no left recursion) | 1 token | Hand-written; one function per rule |
| LL(k) | Top-down | LL(k) | k tokens | Table-driven; generated by tools (ANTLR) |
| LR(k) | Bottom-up | LR(k) | k tokens | Table-driven; shift-reduce; handles left recursion |
| PEG | Top-down | PEG (ordered choice) | 1 token (backtracking) | Unambiguous by construction; left recursion must be eliminated |
Parsing is the second stage of the Compilation pipeline, after tokenization and before semantic analysis.
Details
Recursive descent: hand-written top-down parser; one function per grammar rule. Simple, debuggable, but limited to LL(1) grammars (no left recursion). Used in many language implementations (e.g., Go’s compiler, Python’s ast module).
Shift-reduce (LR): maintain a stack of tokens and states. Shift: push the next token. Reduce: replace the top of the stack with a non-terminal when it matches a production. The conflict (shift-reduce, reduce-reduce) determines whether a grammar is LR(1). LR parsers handle left-recursive grammars naturally, which is why they are the workhorse of C/C++/Java compilers.
PEG (Parsing Expression Grammar): uses ordered choice ( tries first, falls back to only if fails). This makes PEGs unambiguous by construction (the first successful alternative wins). However, PEGs do not always terminate: left-recursive rules (e.g., expr: expr '+' term) cause infinite recursion and must be rewritten (e.g., expr: term ('+' term)*). PEG parsers are used in the peg crate (formerly rust-peg) for Rust, in PEP 617 (the PEG-based parser in CPython 3.9+), and in the parsimonious third-party library for Python.
Language design properties §
Beyond parsing, language theory addresses properties that shape how a language feels to use and extend.
Details
REPL (Read-Eval-Print Loop): an interactive environment that reads an expression, evaluates it, prints the result, and loops. It requires an evaluator that maintains a persistent environment across expressions (variables defined in one expression are visible in the next). Languages with a REPL (Lisp, Scheme, Python, Ruby, Elixir) are well-suited to exploratory programming and teaching.
Homoiconicity: the language’s data structures (e.g., Lisp s-expressions) are the same as its code. (+ 1 2) is both a list and a program. This enables macros (code that manipulates code at compile time), because the macro system can read and rewrite the program’s own data structures. Most mainstream languages (C, Java, Python) are not homoiconic; their syntax is not directly representable as a data structure.
These properties interact with Control Flow and Semantics: a REPL’s persistent environment is essentially a long-lived scope, and homoiconicity affects how control-flow constructs can be extended via macros.
Type theory §
A type system assigns types to expressions and checks that operations are valid. Static types are checked at compile time; dynamic types at runtime.
Details
Structure:
- Product types (tuples, structs): . AoS (Array of Structs):
[{x, y}, {x, y}, ...]. SoA (Struct of Arrays):[{x, x, ...}, {y, y, ...}]. SoA is cache-friendly for SIMD. - Sum types (enums, tagged unions): . Exhaustive pattern matching.
- Function types: .
Functionality:
- Duck typing: “if it quacks like a duck…” (Python, JavaScript). No static check; errors at runtime.
- Strong vs. weak typing: strong types do not allow implicit conversions (Rust, Haskell); weak types do (JavaScript:
"1" + 1 = "11"). - Nominal vs. structural: nominal types are distinct by name (
struct Point1 {x, y}≠struct Point2 {x, y}); structural types are compatible if their shapes match (TypeScript, Go interfaces). - Hindley–Milner type inference: infer types from usage without annotations. infers .
- Polymorphism: parametric (generics: ) and ad hoc (overloading:
+forIntandFloat). - Subtyping: means values of can be used where is expected. Contravariance: function parameters are contravariant ( if ).
Type checking is the semantic-analysis phase in the Compilation pipeline, and the types it assigns determine the semantics of Control Flow and Semantics (e.g., what a match must cover).
Example problem §
Problem: Given the grammar , parse the expression a + b * c and show the AST.
Solution
This is an LR(1) grammar (standard arithmetic with precedence: * binds tighter than +).
Parse (shift-reduce):
- Shift
a→ stack:a - Reduce
a → F→ stack:F - Reduce
F → T→ stack:T - Reduce
T → E→ stack:E - Shift
+→ stack:E + - Shift
b→ stack:E + b - Reduce
b → F → T→ stack:E + T - Shift
*→ stack:E + T * - Shift
c→ stack:E + T * c - Reduce
c → F→ stack:E + T * F - Reduce
T * F → T→ stack:E + T - Reduce
E + T → E→ stack:E✓
AST:
+
/ \
a *
/ \
b c
The * is a child of +, not a sibling, reflecting that b * c is evaluated first.