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

TermMeaningExample for count = count + 10;
LexemeThe actual sequence of characters matchedcount, =, +, 10, ;
TokenA pair (token name, optional attribute value) passed to the parser<id, pointer to count>, <num, 10>
PatternThe rule (a regular expression) describing the lexemes of a tokenletter (letter | digit)* for identifiers
AttributeExtra information for the tokenSymbol-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*/k gives the two tokens i and k, not the single identifier ik.
  • 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+++z is y, ++, +, z: after y, the longest operator starting at +++ is ++.
  • x+++++y is x, ++, ++, +, y: 5 tokens. The scanner does not look ahead to see that x++ + ++y would make a legal expression; the resulting parse (x++ ++) is rejected later, by the parser or the type checker, not by the scanner.
  • b---c is b, --, -, c.
  • ifx is one identifier, not the keyword if followed by x, because the identifier pattern matches the longer prefix.
  • if alone 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:

Text
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:

StateReached byAcceptsOn fOn another letterOn a digit
0startnothing33 (1 on i)4
1iid233
2ifif (listed before id)333
3other identifiersid333
4digitsnumbernonenone4

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.

DFA for the keyword if, identifiers and numbers
Figure 1. The combined five-state DFA for the patterns if, identifier and number, with each accepting state labelled by the token it returns; on input ifx the run 0, 1, 2, 3 ends in an identifier state.

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 NN 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 NN 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.

FragmentTokensCount
printf("i = %d, &i = %x", i, &i);printf ( "..." , i , & i ) ;10
x = y+++z;x = y ++ + z ;7
x+++++yx ++ ++ + y5
i/*j*/ki k2
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:

FragmentTokensCount
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 linex = a ;4
  • '\n' is one character constant: the backslash and n together denote one character.
  • Inside a string literal, /* does not start a comment.
  • In x = a//b, the // starts a comment that swallows b up 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:

C
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 */, and c, * 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-=-b is 5 tokens.
  • Keyword versus identifier. ifx, int1, whileloop are identifiers. if alone 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

  1. 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).

Next steps