CD Notes
Complete guide to using Flex and Bison together, build systems, debugging, and best practices
Introduction
Using Flex and Bison together is the standard industrial approach to building parsers for compilers, interpreters, and domain-specific languages. Individually, each tool handles one half of the problem — Flex converts characters to tokens, Bison converts tokens to parse trees. Together, they form a powerful pipeline that automates the tedious work of hand-writing scanners and parsers. Understanding this workflow — how the tools interact, how to build and debug the generated code, and how to handle common pitfalls — is essential knowledge for any compiler developer.
The End-to-End Workflow
The complete process from specification to running program involves several stages:
| Step 1 | Write specifications |
| Step 2 | Run Bison (MUST run first — generates token definitions) |
| Produces | y.tab.c (parser C code) |
| Step 3 | Run Flex (uses token definitions from Step 2) |
| Produces | lex.yy.c (lexer C code) |
| Step 4 | Compile everything |
| $ gcc -c y.tab.c | y.tab.o |
| $ gcc -c lex.yy.c | lex.yy.o |
| $ gcc -c main.c | main.o (if separate main) |
| Step 5 | Link |
| Step 6 | Run |
| Result | 11 |
The order matters critically. Bison must run first because it generates y.tab.h, which contains the #define statements for token types (like #define NUMBER 258, #define PLUS 259). The Flex specification includes this header so that the lexer returns the same token codes the parser expects.
How Flex and Bison Communicate
The interface between lexer and parser is elegant in its simplicity:
// Bison's generated parser calls:
int yylex(void); // Flex provides this — returns next token type
// Shared state:
extern int yylval; // Token value (semantic attribute)
extern int yylineno; // Current line number
extern char *yytext; // Matched text (current lexeme)
extern FILE *yyin; // Input source fileEvery time the parser needs the next token, it calls yylex(). The lexer reads characters, matches a pattern, executes the associated action (which typically sets yylval and returns the token type). The parser receives the token type and uses yylval for semantic computations.
For tokens that carry values (like numbers), the lexer must set yylval before returning:
For more complex value types, Bison's %union declaration defines what yylval can hold:
/* In calculator.y */
%union {
int intval;
double floatval;
char *strval;
struct ASTNode *node;
}
%token <intval> INTEGER
%token <floatval> FLOAT
%token <strval> IDENTIFIER
%type <node> expression statementA Complete Working Example
Here is a minimal but complete calculator implementation showing both files:
calculator.y (Bison grammar):
calculator.l (Flex lexer):
Build System Integration
Makefile (Most Common)
CC = gcc
CFLAGS = -g -Wall
TARGET = calculator
all: $(TARGET)
$(TARGET): y.tab.o lex.yy.o
$(CC) $(CFLAGS) -o $@ $^ -lm
y.tab.c y.tab.h: calculator.y
bison -d calculator.y
lex.yy.c: calculator.l y.tab.h
flex calculator.l
y.tab.o: y.tab.c
$(CC) $(CFLAGS) -c y.tab.c
lex.yy.o: lex.yy.c y.tab.h
$(CC) $(CFLAGS) -c lex.yy.c
clean:
rm -f $(TARGET) *.o lex.yy.c y.tab.c y.tab.h
.PHONY: all cleanNote the dependency: lex.yy.c depends on y.tab.h, which ensures Bison runs before Flex.
CMake (Modern Projects)
cmake_minimum_required(VERSION 3.10)
project(Calculator C)
find_package(FLEX REQUIRED)
find_package(BISON REQUIRED)
BISON_TARGET(Parser calculator.y ${CMAKE_CURRENT_BINARY_DIR}/y.tab.c
COMPILE_FLAGS "-d")
FLEX_TARGET(Lexer calculator.l ${CMAKE_CURRENT_BINARY_DIR}/lex.yy.c)
ADD_FLEX_BISON_DEPENDENCY(Lexer Parser)
add_executable(calculator
${BISON_Parser_OUTPUTS}
${FLEX_Lexer_OUTPUTS}
)
target_include_directories(calculator PRIVATE ${CMAKE_CURRENT_BINARY_DIR})Debugging Parser Generators
Enabling Bison Debug Trace
Add these to your .y file to see every shift and reduce action:
%debug
%define parse.error verbose
%%
/* grammar rules */
%%
int main() {
yydebug = 1; // Enable step-by-step trace
return yyparse();
}The trace output shows you exactly what the parser is doing:
| Reading a token | Next token is 258 (NUMBER) |
| Reading a token | Next token is 43 ('+') |
| Reducing: expression | NUMBER (value: 3) |
Enabling Flex Debug Output
Using Bison's Verbose Report
bison -v calculator.y # Generates calculator.outputThe .output file shows all parser states, the item sets in each state, and any conflicts with detailed explanations of why they occur.
Common Problems and Solutions
Problem 1: "Undefined reference to yylex"
- Cause: The Flex-generated lexer object file was not linked
- Solution: Ensure lex.yy.o is included in the gcc link command
Problem 2: Token values are always zero
- Cause: Forgot to set
yylvalin lexer actions before returning - Solution: Add
yylval = atoi(yytext);beforereturn NUMBER;
Problem 3: Shift-reduce conflicts
- Cause: Grammar ambiguity (usually precedence-related)
- Solution: Add
%left,%right, or%nonassocdeclarations for operators - Diagnostic: Run
bison -vand examine the .output file for conflict details
Problem 4: Parser accepts nothing / immediate syntax error
- Cause: Token types mismatch between .l and .y files
- Solution: Verify that .l file includes "y.tab.h" and returns the correct token macros
Problem 5: Infinite loop on error
- Cause: Error recovery consumes no tokens
- Solution: Add
errorproductions that consume at least one token:stmt: error ';' { yyerrok; }
Best Practices
- Run Bison first, always — The header file it generates is needed by Flex
- Use
%define parse.error verbose— Get meaningful error messages instead of just "syntax error" - Start simple, add incrementally — Get a minimal grammar working, then add operators one at a time
- Test the lexer independently — Print tokens before connecting to the parser to verify tokenization
- Use
%locations— Bison can track line/column numbers automatically for better error messages - Keep semantic actions small — Call helper functions rather than embedding complex logic in grammar rules
- Use
%destructor— For dynamic memory (strings, AST nodes), define cleanup rules to prevent leaks
Key Takeaways
The Flex-Bison workflow is a disciplined process: write specifications, generate code, compile, link, test. The most common source of bugs is the interface between the two tools — ensuring token definitions match, semantic values are set correctly, and the build order is right. Once you have the workflow mastered, you can focus on the interesting part: designing your language's grammar and implementing its semantics. This same pattern — specification-driven code generation — applies to modern tools like ANTLR, tree-sitter, and language workbenches.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Parser Generator Workflow.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, parser, generators, generator, workflow
Related Compiler Design Topics