Hello! Welcome back.
In our previous lessons, we explored the trifecta of regular languages: DFAs, NFAs, and regular expressions. We proved their equivalence with Kleene's Theorem and even learned how to simplify complex regular expressions using algebraic laws. Now, it's time to see how this theoretical foundation is applied to solve a fundamental problem in computer science.
This lesson bridges theory and practice by focusing on lexical analysis, the first phase of a compiler. You'll learn how regular expressions are used to break down raw source code into a structured sequence of tokens that a parser can understand. Given your experience building backends and processing structured data, you've likely implemented custom parsers or validators; this lesson will formalize that process within the context of language compilation.
By the end of this lesson, you will be able to apply regular expressions to solve practical pattern-matching problems, specifically in the design of a lexical analyzer.
1. The Role of the Lexical Analyzer
Every compiler or interpreter begins its work by reading a source file—a simple stream of characters. Its first task is to transform this stream into something more structured. This is the job of the lexical analyzer, also known as a scanner or tokenizer.
The primary goals of lexical analysis are:
- To read the input characters of the source program.
- To group them into meaningful sequences called lexemes.
- To produce a stream of tokens for each lexeme, which is then passed to the next phase, the parser.
- To strip out non-essential elements like comments and whitespace.
To understand these concepts more formally, let's start with a foundational reading.
These slides, titled 'Part 2 Lexical analysis', clearly define the core terminology and purpose of this compiler phase.
Please read the slides covering the introduction to lexical analysis. You'll find these from the start of the document up to the section 'Describing the patterns' (pages 47 to 54). Focus on understanding the distinction between a token, a lexeme, and a pattern.
As the reading highlights:
- A lexeme is the actual sequence of characters in the source code (e.g.,
count,+,100). - A pattern is a rule that describes the set of all possible lexemes for a token (e.g., "a sequence of letters and digits starting with a letter").
- A token is a pair
⟨token-name, attribute-value⟩that represents a lexeme. For example, the lexemecountcould become the token⟨identifier, "count"⟩. The attribute is often a pointer to the symbol table entry for that lexeme.
The crucial takeaway is that regular expressions are the primary tool used to specify these patterns.
2. Defining Tokens with Regular Expressions
Now, let's connect this to what we've already learned. We can define the patterns for various components of a typical programming language using the regular expression syntax you're familiar with.
Here are some examples for a C-like language:
-
Keywords: The pattern for a keyword is simply the keyword itself.
ifmatches the lexemeif.whilematches the lexemewhile.
-
Identifiers: A common rule is a letter or underscore followed by any number of letters, digits, or underscores.
[a-zA-Z_][a-zA-Z0-9_]*
-
Integers: A sequence of one or more digits.
[0-9]+
-
Relational Operators:
<,>,<=,>=,==,!=
-
Whitespace: One or more space, tab, or newline characters. The scanner usually identifies and discards these.
[ \t\n]+
This slide deck provides more detailed examples, including for floating-point numbers.
The same PDF from before, 'Part 2 Lexical analysis', contains excellent examples of regular expressions for common programming language constructs.
Please review the slide titled 'Examples' (page 58). It shows concrete regular expression patterns for identifiers, integers, floats, and string constants. Note the use of character classes [], quantifiers + and *, and the optional operator ?.

3. From Patterns to a Working Scanner
How does a lexical analyzer use these regular expressions to process an input file? The process directly leverages the automata theory we've been studying:
- A regular expression is defined for each token type in the language.
- Using an algorithm like Thompson's construction, an NFA-ε is created for each regular expression.
- These individual NFAs are combined into a single, large NFA-ε with a new start state that has ε-transitions to the start state of each individual NFA.
- This large NFA-ε is converted into an equivalent, and often much larger, DFA using the subset construction algorithm.
- This final DFA is the "engine" of the lexical analyzer. It is implemented as a program that reads one character at a time and transitions between states. When it enters a final state, it has recognized a lexeme.
The following video provides an excellent visualization of this process.
Introduction to Lexical Analyzer
This video, 'Introduction to Lexical Analyzer', visually demonstrates how the finite state machines for individual tokens are combined into a single machine.
Watch from 02:09 to 10:03. The video shows how individual FSMs for the keyword 'if', identifiers, and integers are first conceptualized. It then shows them being combined into an NFA-ε and finally converted into a single, unified DFA that can recognize all three types of tokens.
4. Handling Ambiguity: Practical Scanning Rules
A key challenge arises when the input can be matched by multiple patterns. For instance, in the C language:
- The string
ifmatches the pattern for the keywordifAND the pattern for anidentifier. - The string
whilecontains the prefixwhil, which also matches theidentifierpattern.
To resolve this, lexical analyzers follow two simple but critical principles.
Regular Expressions - Lexical Analysis
This resource, 'Regular Expressions - Lexical Analysis', clearly explains the two rules used to resolve ambiguity in practice.
Please read slides 10 through 13. They introduce and illustrate the principles of rule priority and 'Maximal Munch'.
To summarize the rules:
- Rule Priority: When multiple patterns match the same lexeme, the pattern listed first in the lexical specification wins. This is why you must list patterns for keywords (e.g.,
if,while) before the general pattern for identifiers. - Maximal Munch (or Longest Match): The scanner always finds the longest possible string of input characters that matches any of the patterns. For example, when scanning
while_loop, the scanner will match the entire stringwhile_loopas an identifier, rather than stopping afterwhileand declaring it a keyword.
Test your understanding!
Suppose a lexical analyzer has the following rules, in this order:
while-> returnsT_WHILE[a-z]+-> returnsT_IDENTIFIER=-> returnsT_ASSIGN
Given the input string while=, what sequence of tokens will be produced? Explain your reasoning using the principles of Rule Priority and Maximal Munch.
Show answer
The scanner will produce the following sequence of tokens: ⟨T_WHILE, "while"⟩, ⟨T_ASSIGN, "="⟩.
Here's why:
- The scanner starts at the beginning of the string
while=. - The prefix
w,wh,whi,whil, andwhileall match theT_IDENTIFIERpattern ([a-z]+). - The prefix
whilealso matches theT_WHILEpattern (while). - Maximal Munch Principle: The scanner will try to find the longest match.
while=does not match any pattern, butwhiledoes. The longest prefix that matches any pattern iswhile. - Rule Priority Principle: The lexeme
whilematches two patterns:while(Rule 1) and[a-z]+(Rule 2). Since Rule 1 is listed first, it wins. The scanner produces the token⟨T_WHILE, "while"⟩. - The scanner now continues from the character after
while, which is=. - The lexeme
=matches theT_ASSIGNpattern. The scanner produces the token⟨T_ASSIGN, "="⟩.
5. Automating the Process: Lexical Analyzer Generators
As you can imagine, manually constructing and coding the giant DFA for a full programming language would be incredibly tedious and error-prone. Fortunately, this entire process is automated by tools called lexical analyzer generators. The most famous of these is lex, and its modern open-source version is called flex.
You provide flex with a specification file (.l file) containing:
- Definitions: C code and name definitions.
- Rules: A list of
pattern-actionpairs, where the pattern is a regular expression and the action is a block of C code to execute when the pattern is matched. - User Code: Auxiliary C functions.
flex then reads this file and generates a C source file (usually lex.yy.c) containing the yylex() function—a highly optimized scanner based on the DFA derived from your rules. Your experience building complex systems in C/C++ and other languages will make the value of such a code generator immediately apparent.
This video provides a fantastic hands-on demonstration.
This tutorial on 'lex/yacc' by Jonathan Engelsma demonstrates how to use lex to build a practical scanner for a configuration file. It's a great example of theory put into practice.
Please watch the video. Pay close attention to these parts: Introduction (00:00 - 04:30): Reinforces the role of the scanner. Lex File Structure (04:30 - 06:15): Explains the three sections of a .l file. Hands-on Example (12:43 - 30:20): This is the most important part. Watch how the author defines regular expressions for keywords (db_type), identifiers, integers, and operators (:) and associates them with C code actions (return TYPE;, return IDENTIFIER;). Observe how the generated scanner is compiled and integrated into a main C program that calls the yylex() function in a loop.
The video demonstrates how elegant the final solution is. Instead of hundreds of lines of manual state-machine code, you write a high-level specification with a few dozen lines of regular expressions and actions. The flex tool handles the complex, low-level conversion to an efficient DFA for you.
Conclusion
Today, we saw one of the most direct and powerful applications of automata theory in computer science. By using regular expressions to define the lexical structure of a language, we can automatically generate fast, correct scanners.
Key Takeaways:
- Lexical analysis is the first phase of compilation, responsible for converting a stream of characters into a stream of tokens.
- The patterns for tokens (keywords, identifiers, numbers, etc.) are formally described using regular expressions.
- A lexical analyzer is conceptually a large DFA that is constructed by combining the automata for all token patterns.
- Practical scanners resolve ambiguity using two principles: rule priority (first match wins) and maximal munch (longest match wins).
- Tools like
lex/flexautomate the generation of efficient scanners from a high-level specification file of regular expressions and associated actions.
Preview of the Next Lesson:
We've seen the impressive power and practicality of regular languages. However, they are not powerful enough to describe all patterns. For example, the simple language of balanced parentheses or the language {aⁿbⁿ | n ≥ 0} cannot be described by a regular expression. In our next lesson, we will introduce the Pumping Lemma for Regular Languages, a formal method used to prove that a language is not regular. This will motivate our move to more powerful models of computation.