Introduction
The lexical analyser (scanner, lexer) is the first phase of a compiler. It reads the source text character by character, groups the characters into meaningful units called lexemes, and passes one token per lexeme to the parser. It also removes comments and whitespace, keeps track of line numbers for error messages, and enters identifiers into the symbol table.
The patterns that describe tokens are regular, so the scanner is a deterministic finite automaton with actions attached to its accepting states. Scanner generators such as Lex and Flex build that automaton automatically from a list of regular expressions. In examination questions the scanner appears mostly through token counting, the longest-match rule, and the classification of errors.
Prerequisites
- Regular Expressions: regular expressions and regular definitions, used to specify token patterns.
- Deterministic Finite Automata: DFAs, used to recognise the patterns. The conversion of a regular expression to an NFA and then to a DFA is covered in the Theory of Computation notes and is not repeated here.
Core concepts
Tokens, lexemes and patterns
| Term | Meaning | Example for count = count + 10; |
|---|---|---|
| Lexeme | The actual sequence of characters matched | count, =, +, 10, ; |
| Token | A pair (token name, optional attribute value) passed to the parser | <id, pointer to count>, <num, 10> |
| Pattern | The rule (a regular expression) describing the lexemes of a token | letter (letter | digit)* for identifiers |
| Attribute | Extra information for the token | Symbol-table entry for an identifier, numeric value for a number |
The token name is what the grammar of the parser uses as a terminal symbol. Typical token classes are: keywords (one token name per keyword, or one shared name with an attribute), identifiers, constants (numbers, character constants), string literals, operators, and punctuation such as parentheses, commas and semicolons.
One token per lexeme. A token is one occurrence. The statement count = count + 10; has 6 tokens (count, =, count, +, 10, ;) even though count appears twice; both occurrences become tokens with the same symbol-table attribute.
What produces no token
- Whitespace: blanks, tabs and newlines (they only separate lexemes).
- Comments:
/* ... */and// ...in C. A comment acts as a separator:i/*j*/kgives the two tokensiandk, not the single identifierik. - Preprocessor directives such as
#include <stdio.h>and#define: they are handled before the compiler's scanner sees the text. Questions that ask for "tokens in the program" usually ignore them; read the question.
The longest-match rule (maximal munch)
At each point the scanner takes the longest prefix of the remaining input that matches some pattern. If two patterns match the same longest prefix, the pattern listed first wins. These two rules settle every token-counting question.
y+++zisy,++,+,z: aftery, the longest operator starting at+++is++.x+++++yisx,++,++,+,y: 5 tokens. The scanner does not look ahead to see thatx++ + ++ywould make a legal expression; the resulting parse (x++ ++) is rejected later, by the parser or the type checker, not by the scanner.b---cisb,--,-,c.ifxis one identifier, not the keywordiffollowed byx, because the identifier pattern matches the longer prefix.ifalone matches both the keyword pattern and the identifier pattern with the same length; the keyword wins because it is listed first (or because keywords are found in a reserved-word table before the name is treated as an identifier).>=is one token,> =(with a blank) is two.
How it works
Specification: regular definitions
Token patterns are written as regular definitions, names for regular expressions that may use earlier names:
letter -> A | B | ... | Z | a | b | ... | z | _
digit -> 0 | 1 | ... | 9
id -> letter ( letter | digit )*
digits -> digit digit*
number -> digits ( . digits )? ( E ( + | - )? digits )?
relop -> < | <= | = | <> | > | >=Recognition: one DFA with actions
A scanner generator converts each pattern to an NFA, joins them under a new start state, and applies the subset construction (see Deterministic Finite Automata). Each accepting DFA state is labelled with the first-listed pattern among those it accepts. To find the longest match, the scanner keeps running the DFA until it has no move, remembering the last accepting state it passed and the input position at that moment; it then retracts the input to that position and returns that token.
Example with three patterns, listed in this order: the keyword if, identifiers [a-z][a-z0-9]*, and numbers [0-9]+. The combined DFA has five live states:
| State | Reached by | Accepts | On f | On another letter | On a digit |
|---|---|---|---|---|---|
| 0 | start | nothing | 3 | 3 (1 on i) | 4 |
| 1 | i | id | 2 | 3 | 3 |
| 2 | if | if (listed before id) | 3 | 3 | 3 |
| 3 | other identifiers | id | 3 | 3 | 3 |
| 4 | digits | number | none | none | 4 |
On input ifx the DFA passes 0, 1, 2, 3 and stops at the end of the lexeme in state 3, so it returns one identifier. On input if( it reaches state 2 and has no move on (, so it returns the keyword if. On i9 it returns one identifier. These five states are all distinguishable (states 1 and 3 accept the same token but differ on f), so the DFA is minimal.

Transition diagrams for relational operators
Hand-written scanners use transition diagrams: a state per prefix seen, edges labelled by characters, and accepting states marked with the token to return. Some accepting states are marked "retract": the scanner has read one character beyond the lexeme (to be sure the lexeme has ended) and must push it back. For relop:
- After
<: on=return<=; on>return<>; on any other character, retract one and return<. - After
=: return=at once, no retraction. - After
>: on=return>=; on any other character, retract one and return>.
Input buffering and sentinels
Reading one character at a time from a file is slow, and the scanner must sometimes look ahead before it knows where a lexeme ends. The standard scheme uses a buffer of two halves (each of characters, typically one disk block) and two pointers: lexemeBegin marks the start of the current lexeme, and forward scans ahead. When forward reaches the end of one half, the other half is reloaded.
Without help, each advance of forward needs two tests: "end of this half?" and "which character is it?". A sentinel (a special end-of-file character placed at the end of each half) folds both into one: the scanner tests only for the sentinel, and only on seeing it does it check whether the half ended or the input ended. A lexeme longer than the buffer cannot be handled; in practice a lexeme is limited to well under characters.
Lexical errors and recovery
A lexical error occurs when no pattern matches any prefix of the remaining input: an illegal character such as @ or ` `` in C source outside a literal, or a string literal or comment that never ends. The simplest recovery is panic mode: delete characters from the remaining input until a well-formed token can be found. Other single-character repairs are deleting one character, inserting a missing one, replacing one, or transposing two adjacent characters.
Lex in outline
A Lex specification has three parts separated by %%: declarations (regular definitions and C declarations), translation rules (pattern followed by an action in braces), and auxiliary functions. The generated function yylex() returns one token per call; yytext points to the lexeme and yyleng holds its length. Conflicts are resolved by the two rules above: longest prefix, then earliest rule. The lookahead operator / in a pattern r1/r2 matches r1 only when it is followed by r2, without consuming r2.
Worked examples
Counting convention: one token per lexeme; comments, whitespace and preprocessor lines produce none; a string literal (including its quotes and any escaped quotes inside) is one token.
| Fragment | Tokens | Count |
|---|---|---|
printf("i = %d, &i = %x", i, &i); | printf ( "..." , i , & i ) ; | 10 |
x = y+++z; | x = y ++ + z ; | 7 |
x+++++y | x ++ ++ + y | 5 |
i/*j*/k | i k | 2 |
if (x>=10) y = x<<2; else y = -x; | if ( x >= 10 ) y = x << 2 ; else y = - x ; | 18 |
int x=1, y=02, z=0x1F; | int x = 1 , y = 02 , z = 0x1F ; | 13 |
w.x->y[3]--; | w . x -> y [ 3 ] -- ; | 10 |
float f = 3.14e-2; | float f = 3.14e-2 ; | 5 |
More fragments that test the rules at their edges:
| Fragment | Tokens | Count |
|---|---|---|
x = y/ *p; | x = y / * p ; | 7 |
a-=-b; | a -= - b ; | 5 |
a = b---c; | a = b -- - c ; | 7 |
c = '\n'; | c = '\n' ; | 4 |
s = "a /* b */ c"; | s = "a /* b */ c" ; | 4 |
int x[10], *p = &x[0]; | int x [ 10 ] , * p = & x [ 0 ] ; | 15 |
x = a//b then ; on the next line | x = a ; | 4 |
'\n'is one character constant: the backslash andntogether denote one character.- Inside a string literal,
/*does not start a comment. - In
x = a//b, the//starts a comment that swallowsbup to the end of the line.
A comment opened by accident. Without the blank, x=y/*p; does not divide by *p: longest match reads /* as the start of a comment. If no */ follows anywhere later in the file, the scanner reports an unterminated comment, a lexical error. If a */ does appear later, everything up to it silently disappears, and the parser reports whatever error results. The blank in y/ *p (or parentheses, y/(*p)) gives the intended 7 tokens.
A larger one:
int main() { int a = 10, *p = &a; /* sum */ return a+++*p; }Tokens: int main ( ) { int a = 10 , * p = & a ; return a ++ + * p ; }, a total of 24. This is a token-count exercise only: evaluating a+++*p has undefined behaviour in C because p aliases a, and the modification by a++ is unsequenced relative to the read through *p. It is not an output-prediction example. The comment contributes nothing, a+++*p splits as a, ++, +, *, p, and the * in *p is one token whether it means multiplication or dereference; the scanner does not decide which.
Escaped quote. In char *s = "a\"b"; the literal "a\"b" is one token, because \" does not end the string: 6 tokens in all.
Important properties and observations
- Tokens are a regular language; nesting (balanced parentheses, nested comments) is not regular, so a scanner cannot match nested comments with a pure DFA. C comments do not nest: in
/* a /* b */ c */the comment ends at the first*/, andc,*and/are then tokens. - The scanner does not know the meaning of a token in context.
*is one token for both multiplication and indirection,-for both binary and unary minus, and(for both grouping and calls. - Keyword handling. Either a separate DFA path for each keyword (listed before identifiers), or one identifier pattern plus a reserved-word table consulted when an identifier is found. The table approach keeps the DFA small.
- Common misconceptions. "A misspelt keyword is a lexical error": no, it is a valid identifier (see the note Compiler Phases and Structure). "The scanner picks the split that makes the program legal": no, it applies longest match blindly. "Whitespace inside a string literal ends the token": no, the literal runs to its closing quote.
- A scanner needs lookahead of one character in most languages (to see that
<is not<=). Fortran's free treatment of blanks is the classical example needing much longer lookahead.
GATE examination perspective
- Token counting. Apply longest match, count each lexeme once, skip comments and whitespace, count a string literal as one token. Watch for
+++,---,->,<<=, and for/*inside a string (it does not start a comment there). /*opens a comment only outside string/character literals and outside an existing comment (during preprocessing-token formation after line splicing). Even when the programmer meant division followed by a dereference (y/*p). The result is an unterminated comment, or code silently swallowed up to the next*/.- Escapes and compound operators.
'\n'is one token;-=,<<=and->are single tokens;a-=-bis 5 tokens. - Keyword versus identifier.
ifx,int1,whileloopare identifiers.ifalone is the keyword because of rule priority. - Which errors are lexical. Illegal characters and unterminated literals or comments. Misspelt keywords, missing semicolons and undeclared names are not.
- Pattern questions. Writing a regular expression for identifiers, numbers or comments is a regular-expression exercise; minimising the scanner DFA is a DFA exercise (see the Theory of Computation notes).
- Lex conflict rules. Longest match first, then the earliest listed rule. A rule placed after a more general one that matches the same strings never fires for those strings.
- Input buffering. Two buffer halves, two pointers, sentinels reduce two tests per character to one in the common case.
Summary
Sources
- A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers, Principles, Techniques, and Tools, 2nd ed., Pearson Addison-Wesley, 2007: Section 3.1 (role of the lexical analyser; tokens, patterns and lexemes; lexical errors), Section 3.2 (input buffering, buffer pairs, sentinels), Section 3.3 (specification of tokens, regular definitions), Section 3.4 (recognition of tokens, transition diagrams, reserved words), Section 3.5 (Lex, conflict resolution, the lookahead operator), Section 3.8.3 (DFAs for lexical analysers).