Yasir Explains/Compiler Design/Automata/Lexical Analysis
Automata

Lexical Analysis

On this page

What is Lexical Analysis?Tokens, Lexemes, and PatternsWorked Example — Tokenising a StatementHow the Lexer Works — Step by StepLexical ErrorsSummary
Automata

Lexical Analysis

The first phase of a compiler — the lexer reads raw source characters and groups them into a stream of tokens that every later phase understands.

What is Lexical Analysis?

Lexical analysis (also called scanning or tokenisation) is Phase 1 of the compiler pipeline. Its job is straightforward:

Read the source code character by character and group those characters into tokens — the smallest meaningful units of the language.

Think of it like reading an English sentence. You don't process the sentence one letter at a time — you naturally chunk letters into words, then punctuation. The lexer does the same for source code.


Position in the Compiler Pipeline

Example
Source Code (raw characters)
│
▼
┌─────────────────────┐
│ Lexical Analyser │ ← Phase 1 (this topic)
│ (Lexer) │
└─────────────────────┘
│
▼
Token Stream
│
▼
Syntax Analyser (Parser) → and so on ...

The lexer sits between the raw source text and everything else. Without it, the parser would have to deal with whitespace, comments, and individual characters — a nightmare. The lexer cleans all of that up.

Tokens, Lexemes, and Patterns

Three terms are essential for understanding lexical analysis:

TermDefinitionExample
TokenA category — the type of the matched textKEYWORD, IDENTIFIER, NUMBER
LexemeThe actual text from the source code that matchedwhile, total, 42
PatternThe rule (often a regular expression) that describes what text matches this token[a-zA-Z_][a-zA-Z0-9_]* matches identifiers

Common Token Types

Token TypePattern (informal)Lexeme Examples
KEYWORDFixed reserved wordsint, if, while, return
IDENTIFIERLetter/underscore then letters/digitstotal, _count, myVar2
INTEGER_LITERALSequence of digits0, 42, 1000
FLOAT_LITERALDigits, dot, digits3.14, 0.5, 2.0
OPERATORArithmetic/comparison symbols+, -, *, ==, <=
PUNCTUATIONDelimiters and separators(, ), {, }, ;, ,
STRING_LITERALText between double quotes"hello", "world"

Whitespace (spaces, tabs, newlines) and comments are not tokens — the lexer discards them silently.

Worked Example — Tokenising a Statement

Let's trace the lexer on a real C-style statement:

Example
total = price * qty + discount;

The lexer scans left to right. Each time it finds the longest matching pattern, it emits a token and advances:

Example
Lexeme Token Type
────────── ───────────────
total IDENTIFIER
(space) — discarded —
= ASSIGN_OP
(space) — discarded —
price IDENTIFIER
(space) — discarded —
* ARITH_OP
(space) — discarded —
qty IDENTIFIER
(space) — discarded —
+ ARITH_OP
(space) — discarded —
discount IDENTIFIER
; SEMICOLON

Token stream output:

Example
<IDENTIFIER, "total"> <ASSIGN_OP, "="> <IDENTIFIER, "price">
<ARITH_OP, "*"> <IDENTIFIER, "qty"> <ARITH_OP, "+">
<IDENTIFIER, "discount"> <SEMICOLON, ";">

The parser never sees whitespace — it receives a clean sequence of typed tokens.


Another Example — a Function Declaration

Example
int add(int a, int b) {
return a + b;
}

Token stream:

Example
<KEYWORD,"int"> <IDENTIFIER,"add"> <LPAREN,"(">
<KEYWORD,"int"> <IDENTIFIER,"a"> <COMMA,",">
<KEYWORD,"int"> <IDENTIFIER,"b"> <RPAREN,")">
<LBRACE,"{">
<KEYWORD,"return"> <IDENTIFIER,"a"> <ARITH_OP,"+"> <IDENTIFIER,"b"> <SEMICOLON,";">
<RBRACE,"}">

How the Lexer Works — Step by Step

The lexer implements a longest-match rule (also called maximal munch):

At each position, consume the longest sequence of characters that matches any pattern.

This is why <= is ONE token (LESS_EQUAL), not two (LESS, EQUAL). The lexer always prefers the longer match.


Lexer Algorithm

Example
1. Start at position i = 0 in the source string
2. Try to match a pattern starting at position i
3. Among all patterns that match, pick the one that matches the LONGEST substring
4. Emit token (type, lexeme)
5. Advance i past the matched lexeme
6. Repeat from step 2 until end of source
7. Emit special EOF token

Under the hood, most lexers are implemented as a Deterministic Finite Automaton (DFA). Each token type corresponds to a regular expression; those regular expressions are combined into a single DFA that runs in O(n) time on a source file of length n.


Keyword vs Identifier — How the Lexer Decides

Keywords like int, while, return look exactly like identifiers by their character pattern. Lexers handle this two ways:

Method 1 — Keyword table:

  • Recognise while as an IDENTIFIER first
  • Then look it up in a keyword table
  • If found → reclassify as KEYWORD

Method 2 — Higher priority pattern:

  • Keyword patterns are given higher priority than the IDENTIFIER pattern
  • The DFA resolves the ambiguity by picking the keyword path first

Lexical Errors

A lexical error occurs when the lexer encounters a sequence of characters that does not match any valid token pattern.

Common Lexical Errors

ErrorExampleProblem
Invalid character@sum (@ is not a valid start)Character not in the language's alphabet
Unterminated string"hello (missing closing quote)String literal never closed
Ill-formed number3.14.15Two decimal points
Invalid escape"\q" (\q is not a valid escape)Unknown escape sequence

What Happens on a Lexical Error?

Most lexers use error recovery to continue scanning and report multiple errors at once:

  1. Panic mode — skip characters until a valid token can be formed, then continue
  2. Insert/delete — hypothetically insert or delete one character to make progress
  3. Emit an ERROR token — the parser will then reject the token stream at the syntax phase

Lexical errors are the easiest category to recover from — the lexer simply skips the bad character and resynchronises at the next recognisable token.

Summary

ConceptKey Takeaway
TokenCategory/type label (IDENTIFIER, KEYWORD, …)
LexemeActual matched text from source
PatternRegular expression that defines the token
Maximal munchAlways match the longest possible string
ImplementationDFA running in O(n) on input of length n
OutputA flat token stream — no whitespace or comments

The lexer's output — a stream of (type, lexeme) pairs — is everything the parser needs. Lexical analysis effectively converts a character-level view of the program into a word-level view, making every subsequent phase far simpler to implement.