Yasir Explains/Compiler Design/Automata/Regular Expressions
Automata

Regular Expressions

On this page

What are Regular Expressions?The Three Core OperationsShorthand OperatorsBuilding Token Patterns — Worked ExamplesFrom Regular Expression to DFARE to NFA — Thompson's Construction
Automata

Regular Expressions

Regular expressions are the compact notation used to specify exactly which strings belong to a token class. Master the three core operations and the shorthand that compilers rely on.

What are Regular Expressions?

A regular expression (RE) is a formula that describes a set of strings — called a regular language. In compiler design, every token type has a corresponding regular expression that defines what text can match it.

For example:

  • The regular expression [a-zA-Z_][a-zA-Z0-9_]* describes all valid C identifiers.
  • The regular expression [0-9]+ describes all unsigned integers.

Regular expressions are important because:

  1. They are precise — no ambiguity about what matches.
  2. They are mechanically convertible to finite automata, which run in O(n) time.
  3. They form the theoretical backbone of every lexer generator (flex, ANTLR, re2c).

Alphabet and Language

Before defining operations, two terms matter:

  • Alphabet (Σ) — the set of all characters the language can use. For most programming languages Σ = ASCII or Unicode.
  • Language (L) — a (possibly infinite) set of strings over Σ. A regular expression denotes a language.

The Three Core Operations

Every regular expression is built from three primitive operations applied to base symbols:

1. Union (written r | s or r + s)

Denotes all strings that match r OR all strings that match s.

Example
L(r | s) = L(r) ∪ L(s)

Example: a | b matches the strings {a, b} — nothing else.

Example: cat | dog matches "cat" or "dog".


2. Concatenation (written rs or r · s)

Denotes all strings formed by taking any string from L(r) and appending any string from L(s).

Example
L(rs) = { xy | x ∈ L(r) and y ∈ L(s) }

Example: ab matches only "ab" — 'a' followed by 'b'.

Example: (a|b)(c|d) matches {ac, ad, bc, bd}.


3. Kleene Star (written r*)

Denotes zero or more repetitions of any string that matches r.

Example
L(r*) = { ε } ∪ L(r) ∪ L(rr) ∪ L(rrr) ∪ ...

Example: a* matches {ε, a, aa, aaa, aaaa, …} — zero or more 'a's.

Example: (ab)* matches {ε, ab, abab, ababab, …}.


Operator Precedence (highest to lowest)

Example
1. * (Kleene star) — tightest binding
2. · (concatenation)
3. | (union) — loosest binding

So ab*|c parses as (a(b*))|c, not a(b*(|c)).

Shorthand Operators

The three core operations are all you need theoretically, but practical lexers use shorthand to keep patterns readable:

ShorthandMeaningEquivalent core expressionExample
r+One or more rrr*[0-9]+ = one or more digits
r?Zero or one r`rε`
[abc]Character class: a or b or c`ab
[a-z]Character range: a through z`ab
[^abc]Negated class: any char except a, b, ccomplement[^"] = any non-quote char
.Any single character (except newline)union of all chars.* = any string
\dDigit [0-9][0-9]\d+ = integer
\wWord char [a-zA-Z0-9_]explicit union\w+ = identifier-like
\sWhitespace (space, tab, newline)explicit union\s+ = skip whitespace

Building Token Patterns — Worked Examples

Example 1 — Unsigned Integer

Language: strings of one or more digits.

Example
Pattern: [0-9]+
Breakdown:
[0-9] — any single digit (0, 1, 2, …, 9)
+ — one or more of the preceding
Matches: 0 42 1000 999999
No match: "" "abc" "3.14"

Example 2 — C Identifier

Language: starts with a letter or underscore, followed by zero or more letters, digits, or underscores.

Example
Pattern: [a-zA-Z_][a-zA-Z0-9_]*
Breakdown:
[a-zA-Z_] — first character: letter or underscore
[a-zA-Z0-9_]* — remaining characters: letter, digit, or underscore (zero or more)
Matches: x total _count myVar2 MAX_SIZE
No match: 2x hello-world 123abc

Example 3 — Floating-Point Literal

Language: optional minus sign, one or more digits, a dot, one or more digits.

Example
Pattern: -?[0-9]+.[0-9]+
Breakdown:
-? — optional minus sign
[0-9]+ — integer part (one or more digits)
. — literal dot (escaped so it's not "any char")
[0-9]+ — fractional part (one or more digits)
Matches: 3.14 0.5 -2.71 100.0
No match: 3 .5 3. -3

Example 4 — C String Literal

Language: a double-quote, followed by any non-quote characters (simplified), followed by a closing double-quote.

Example
Pattern: "[^"]*"
Breakdown:
" — opening double-quote (literal)
[^"]* — zero or more characters that are NOT a double-quote
" — closing double-quote (literal)
Matches: "" "hello" "foo bar" "123"
No match: "unclosed hello

Example 5 — C Keyword OR Identifier

In a real lexer, keywords are listed explicitly (higher priority) and identifiers catch everything else:

Example
KEYWORD: int | float | if | else | while | return | void
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*
Rule: keyword patterns have higher priority.
When the lexer sees "while", both patterns match —
the KEYWORD pattern wins because it has higher priority.

From Regular Expression to DFA

In a compiler's lexer, regular expressions do not run directly. They go through a standard pipeline:

Example
Regular Expressions
│
▼ (Thompson's Construction)
NFA (Non-deterministic Finite Automaton)
│
▼ (Subset Construction)
DFA (Deterministic Finite Automaton)
│
▼ (DFA Minimization)
Minimal DFA ← the actual scanner

Each step is mechanical and lossless — the minimal DFA accepts exactly the same strings as the original regular expression, and it runs in O(n) time.

This pipeline is what tools like flex, ANTLR, and re2c implement for you when you write a lexer specification.


Regular Languages and the Chomsky Hierarchy

Regular expressions describe exactly the regular languages — the Type 3 languages in the Chomsky Hierarchy. They are less powerful than context-free grammars (CFGs) but sufficient for all lexical-level patterns:

Can regular expressions describe?Answer
Keywords, identifiers, numbers✓ Yes
Balanced parentheses ()(()))✗ No — requires a CFG
Nested structures like aⁿbⁿ✗ No — requires a CFG

This is exactly why compilers use two separate phases: the lexer (regular expressions / DFA) for tokens, and the parser (CFG / PDA) for structure.

RE to NFA — Thompson's Construction

Thompson's Construction is the standard algorithm for converting a regular expression into an NFA. It works inductively — it builds a tiny NFA for each base symbol and combines them using rules that mirror the RE operators exactly.

Key Properties of Every Thompson NFA

Every NFA produced by Thompson's Construction has exactly three guarantees:

Example
1. One start state — no incoming ε-edges from outside
2. One accept state — no outgoing edges from the accept state
3. At most 2 outgoing transitions per state

These properties make composition predictable — you can always wire two sub-NFAs together cleanly.


The Four Construction Rules


Rule 1 — Single character a

Create exactly two states and connect them with a single labelled edge. No ε-edges are needed.

Example
States added : 2 → N_start, N_accept
ε-edges added: 0

Rule 2 — Union r | s

Add a new start state and a new accept state. Use four ε-edges to run both sub-NFAs in parallel — the machine non-deterministically picks one path.

Example
States added : 2 → N_start, N_accept
ε-edges added: 4 → N_start→r N_start→s accept(r)→N_accept accept(s)→N_accept

Purple boxes represent the two complete sub-NFAs. Arrows into a box enter at its start state; arrows out of a box leave from its accept state.


Rule 3 — Concatenation r · s

No new states are needed. A single ε-edge chains the accept of r directly into the start of s, so they run one after the other.

Example
States added : 0
ε-edges added: 1 → accept(r) → start(s)

Rule 4 — Kleene Star r*

Add a new start state and a new accept state. Four ε-edges implement the four behaviours of *: enter, skip (zero times), loop (repeat), and exit.

Example
States added : 2 → N_start, N_accept
ε-edges added: 4 → ① ② ③ ④ (see below)
① enter N_start → start(r) begin the first iteration
② skip N_start → N_accept zero repetitions — ε is accepted
③ loop accept(r)→ start(r) go back for another iteration
④ exit accept(r)→ N_accept finish after completing an iteration

The dotted line between start_r and accept_r represents the entire NFA body for r (its internal states and transitions, whatever they are).


Worked Example — Convert (a|b)* to NFA

We parse the expression bottom-up and apply the rules in this order:

Example
Parse structure: (a | b) *
Steps:
1. NFA for 'a' → Rule 1
2. NFA for 'b' → Rule 1
3. NFA for 'a | b' → Rule 2 (union of steps 1 and 2)
4. NFA for '(a|b)*' → Rule 4 (Kleene star on step 3)

Step 1 — NFA for a

Apply Rule 1: two states, one 'a' edge.

Example
States: {N0, N1}
Start: N0
Accept: N1

Step 2 — NFA for b

Apply Rule 1 again: two new states, one 'b' edge.

Example
States: {N2, N3}
Start: N2
Accept: N3

Step 3 — NFA for a | b

Apply Rule 2 (Union): add new start N4 and new accept N5. Draw four ε-edges to wire both sub-NFAs in parallel.

Example
States: {N0, N1, N2, N3, N4, N5}
Start: N4
Accept: N5
Accepts: strings "a" or "b"

Reading the diagram:

  • N4 non-deterministically jumps to N0 (try the 'a' path) or N2 (try the 'b' path).
  • Whichever path matches, its accept state flows into N5 via ε.

Step 4 — NFA for (a|b)*

Apply Rule 4 (Kleene Star): add new start N6 and new accept N7. Draw four ε-edges:

EdgePurpose
N6 →ε→ N4Enter the loop body (start a new iteration)
N6 →ε→ N7Skip entirely (zero repetitions — ε is accepted)
N5 →ε→ N4Loop back after completing one iteration
N5 →ε→ N7Exit after one or more iterations
Example
States: {N0, N1, N2, N3, N4, N5, N6, N7} — 8 states total
Start: N6
Accept: N7
Accepts: ε, a, b, aa, ab, ba, bb, aab, ... (any string over {a,b})

Tracing the NFA on Input "ab"

We track the ε-closure of the active state set after every symbol.

Example
──────────────────────────────────────────────────────────────
Initial ε-closure({N6}):
N6 →ε→ N4, N6 →ε→ N7
N4 →ε→ N0, N4 →ε→ N2
Active = {N6, N4, N7, N0, N2}
──────────────────────────────────────────────────────────────
Read 'a':
N0 on 'a' → N1 (only N0 has an 'a' edge)
ε-closure({N1}):
N1 →ε→ N5
N5 →ε→ N4 →ε→ N0, N4 →ε→ N2
N5 →ε→ N7
Active = {N1, N5, N4, N0, N2, N7}
──────────────────────────────────────────────────────────────
Read 'b':
N2 on 'b' → N3 (only N2 has a 'b' edge in active set)
ε-closure({N3}):
N3 →ε→ N5
N5 →ε→ N4 →ε→ N0, N4 →ε→ N2
N5 →ε→ N7
Active = {N3, N5, N4, N0, N2, N7}
──────────────────────────────────────────────────────────────
End of input.
N7 ∈ Active → ACCEPTED ✓

Tracing the NFA on Input "ba"

Example
Initial: Active = {N6, N4, N7, N0, N2}
Read 'b':
N2 on 'b' → N3
ε-closure({N3}) = {N3, N5, N4, N0, N2, N7}
Active = {N3, N5, N4, N0, N2, N7}
Read 'a':
N0 on 'a' → N1
ε-closure({N1}) = {N1, N5, N4, N0, N2, N7}
Active = {N1, N5, N4, N0, N2, N7}
End of input.
N7 ∈ Active → ACCEPTED ✓

Tracing the NFA on ε (empty string)

Example
Initial: Active = {N6, N4, N7, N0, N2}
End of input immediately.
N7 ∈ Active → ACCEPTED ✓ (zero repetitions is valid for *)

Summary of Thompson's Construction

RE constructNew statesNew ε-edgesNotes
Single char a20Base case
Union `rs`24
Concat rs01Sequential chain
Star r*24Loop + skip

A regular expression of length m produces an NFA with at most 2m states — one pair per character or operator. Thompson's Construction always runs in O(m) time and produces a compact NFA ready for subset construction.