Technopedia

The technologist’s encyclopedia

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 G=(V,Σ,R,S)G = (V, \Sigma, R, S) is a tuple of:

The Chomsky hierarchy classifies grammars by the restrictiveness of their production rules:

ClassProduction formAutomatonExample
Regular (Type-3)A → a | aB (right-linear)DFA / NFATokenizers, regex
Context-free (Type-2)A → α (α ∈ (V ∪ Σ)*)Pushdown automatonArithmetic, statements, expressions
Context-sensitive (Type-1)αAβ → αγβ, |γ| ≥ 1Linear-bounded automatonSome natural-language fragments
Unrestricted (Type-0)α → β (any form)Turing machineGeneral 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 V={E,T,F}V = \{E, T, F\}, Σ={+,∗,(,),id}\Sigma = \{+, *, (, ), id\}, S=ES = E. 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 classAutomatonMemoryParser strategy
RegularDFA / NFANone (state only)Table-driven lexer (flex, lex)
Context-freePushdown automaton (PDA)StackLL (top-down), LR (bottom-up), PEG
Context-sensitiveLinear-bounded automatonTape bounded by inputGeneral backtracking
UnrestrictedTuring machineUnbounded tapeUndecidable in general
Details

DFA (Deterministic Finite Automaton): a set of states, a transition function δ:Q×Σ→Q\delta: Q \times \Sigma \to Q, 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 ϵ\epsilon-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:

StrategyDirectionGrammar classLookaheadNotes
Recursive descentTop-downLL(1) (no left recursion)1 tokenHand-written; one function per rule
LL(k)Top-downLL(k)k tokensTable-driven; generated by tools (ANTLR)
LR(k)Bottom-upLR(k)k tokensTable-driven; shift-reduce; handles left recursion
PEGTop-downPEG (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 (e1/e2e_1 / e_2 tries e1e_1 first, falls back to e2e_2 only if e1e_1 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:

Functionality:

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 E→E+T∣T,  T→T∗F∣F,  F→(E)∣idE \to E + T \mid T,\; T \to T * F \mid F,\; F \to (E) \mid id, 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):

  1. Shift a → stack: a
  2. Reduce a → F → stack: F
  3. Reduce F → T → stack: T
  4. Reduce T → E → stack: E
  5. Shift + → stack: E +
  6. Shift b → stack: E + b
  7. Reduce b → F → T → stack: E + T
  8. Shift * → stack: E + T *
  9. Shift c → stack: E + T * c
  10. Reduce c → F → stack: E + T * F
  11. Reduce T * F → T → stack: E + T
  12. 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.