Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Regex for Lexical Analysis

Recommended resources

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.

Lexical Analysis in Automata Theory and Compiler Design
This diagram illustrates the phases of a compiler. Lexical analysis is the very first step, converting the source code character stream into a token stream for the syntax analyzer (parser).

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.

Part 2 Lexical analysis

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 lexeme count could 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.

    • if matches the lexeme if.
    • while matches the lexeme while.
  • 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.

Part 2 Lexical analysis

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

Regular Expressions (Regex) Cheat Sheet
A handy cheat sheet for common regular expression syntax. This is a good reference for constructing patterns.


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:

  1. A regular expression is defined for each token type in the language.
  2. Using an algorithm like Thompson's construction, an NFA-ε is created for each regular expression.
  3. 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.
  4. This large NFA-ε is converted into an equivalent, and often much larger, DFA using the subset construction algorithm.
  5. 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 if matches the pattern for the keyword if AND the pattern for an identifier.
  • The string while contains the prefix whil, which also matches the identifier pattern.

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:

  1. 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.
  2. 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 string while_loop as an identifier, rather than stopping after while and declaring it a keyword.
Test your understanding!

Suppose a lexical analyzer has the following rules, in this order:

  1. while -> returns T_WHILE
  2. [a-z]+ -> returns T_IDENTIFIER
  3. = -> returns T_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:

  1. The scanner starts at the beginning of the string while=.
  2. The prefix w, wh, whi, whil, and while all match the T_IDENTIFIER pattern ([a-z]+).
  3. The prefix while also matches the T_WHILE pattern (while).
  4. Maximal Munch Principle: The scanner will try to find the longest match. while= does not match any pattern, but while does. The longest prefix that matches any pattern is while.
  5. Rule Priority Principle: The lexeme while matches 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"⟩.
  6. The scanner now continues from the character after while, which is =.
  7. The lexeme = matches the T_ASSIGN pattern. 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:

  1. Definitions: C code and name definitions.
  2. Rules: A list of pattern-action pairs, where the pattern is a regular expression and the action is a block of C code to execute when the pattern is matched.
  3. 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.

Part 01: Tutorial on lex/yacc

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/flex automate 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.