Github repository: https://github.com/corvofeng/compiler
Main Course Objectives
- Deepen the understanding of compiler principles and the process of constructing a compiler
- Strengthen programming ability
- Learn to write tool software
Course Requirements
SeuLex
- Parsing of the Lex input file
- Parsing of regular expressions
- Implementing an algorithm to convert one regular expression to an NFA
- Merging multiple NFAs
- Implementing NFA determinization and minimization algorithms
- Mapping between return states and returned content
- Applying SeuLex
SeuYacc
- Parsing of the Yacc input file
- Constructing the pushdown automaton for the LR(1) grammar corresponding to a context-free grammar
- Constructing the corresponding parsing table from the LR(1) pushdown automaton
- Constructing the LR(1) driver program (the table-lookup program)
- Mapping from LR(1) to LALR(1)
- Building the symbol table and its management routines
- Adding semantic action routines
- Applying SeuYacc
Tools Used
This experiment used many open source tools as aids; below I list the most important ones. This section can be skipped entirely.
Linux

- Operating system
In this project, to keep everyone’s environment uniform and to use existing mature development tools, our whole group used Linux as the development environment.
GCC
- Compiler
Used as the compiler: the project also makes heavy use of new C++11 features, and a mature compiler greatly improved
development efficiency.
GDB
Debugging tool
A sharp weapon for debugging memory leaks and
Segmentation faults. Sometimes a thorough debugging session on a given breakpoint is more
efficient, and GDB’s command-line mode is sometimes clearer.
Git

Version control
Collaboration was easy with Git. Even though the group was small, we still worked collaboratively, and using
Gitgreatly reduced
the difficulty of communication.
CMake (Makefile)

- Build/install tool
The whole project is built with CMake, which has good cross-platform support and can feed many IDEs. Support for dynamic and static libraries greatly simplified project construction, letting developers focus on code as soon as possible.
Qt Creator

IDE
Like CMake, a powerful cross-platform development tool; all group members developed on Linux. Compared with
Eclipse,
Qt Creator‘s excellent CMake support spared us the trouble of generating projects.
valgrind

Software analysis tool
Since the project is written in C++, memory control is a real headache;
valgrindconveniently analyzes leaks. During
the project there was more than one memory leak, and with valgrind the leak locations were found quickly. Given
our limited skill, we only used it for leak analysis.
lex & yacc
- Programming tools
The project used lex and yacc as syntax and semantic analysis examples. Before the project started, we needed to understand Lex and Yacc first, and the existing tools helped us get started quickly.
Lex Design
Lex is the first core component of the whole project; this document introduces it in the sections below.
Lex Architecture Analysis

This diagram vividly introduces the use case of
Lexand the goal we are going to implement: aLexcompiler — a tool that reads aLexsource program (lex.l) and generates lex.yy.c. Setting aside jargon likeNFAandDFA, we already understand the input and output of the project; the remaining implementation details will be unfolded in the rest of this document.
Input File (the lex source program)
- Due to implementation difficulty, the source program used in this project differs somewhat from a standard lex source program. The lex source program is stored
in
./input/require.l(source fragment):
1 | %{ |
A standard
lex source filehas no middle%! %!section. When writing programs yourself,[0-9], [a-zA-Z]are hard to express, so here we adopt a form of pre-defined functions, letting custom programs parse single characters.
Output File (the generated lexical analyzer)
- This output file mainly consists of the
scanerfunction.
1 | // scanner function |
This file is the output of the
Lexprogram. You can see thatcase 1contains the use of the parsing expressions: while Lex runs, it regex-parses thelex source fileabove and applies the operations of the matched regular expressions. In case 1 we get the operation output corresponding to the regular expression below:
1 | ({letter}|_)({letter}|_|{digit})* { |
Processing Flow
- Now that we know the input and output of the
Lexprogram, we introduce the flow. The document will walk through the whole process starting from regular expressions with a simple example.
Suppose we have the regular expression
a*(b|(cd?))+and the expressionefConvert the regular expressions to
NFAs

Conversion process
In the Re2NFA class, the functions re2post and post2nfa are defined, which respectively convert an infix regular expression to postfix
and convert a postfix expression to an NFA. For the concrete implementation see ./lex/nfa.h in the project code.
- Merge the
NFAexpressions

Merging process
Since merging also produces an NFA, we use the NFA2LIST class to merge; the user can
keep calling the merge function. When merging, a new start node is created, then the character
sets of the two NFAs are merged.
NFAtoDFA
Conversion process
The subset construction method is used.
The initial state of the NFA is used as the initial state of the DFA, and a closure operation is performed: all states reachable only
via edges labeled $\epsilon$. In the implementation I call this findSimple — expanding from a core point to all surrounding points.
This function maps a core point to the points around it.
Taking the NFA above as an example, node 0 becomes the initial state of the DFA, and the findSimple function expands it,
adding nodes 1, 3, 4, 7, 9 to the initial state of the DFA as well.
Here I want to explain that setting core points is very necessary:
since every new DFA state must be compared with all previously existing DFA states, comparing all NFA states
corresponding to each DFA state would be extremely costly. With core points set, the number of
comparisons is greatly reduced.
Below I describe it in pseudocode:
1 | while (there exists an unvisited DFA state) { |
Taking DFA state 0 above as an example, we traverse the symbols a, b, c, d, e, f.
At the same time, we will find that DFA states with these core nodes can be constructed (core nodes in parentheses):
- 0 -> e -> 1 (16)
- 0 -> a -> 2 (7)
- 0 -> c -> 4 (15)
- 0 -> b -> 3 (12)
With the core nodes in hand, findSimple can be used to expand them.
Important Data Structure Definitions
Basic Data Structures
1 | /* |
NFA and DFA
1 | /* |
Lex
1 | class Lex { |
Using the Generated Lexical Analyzer
We have obtained the generated lexical analyzer (e.g. out.c); next we parse a file:
1 | ☁ input gcc out.c -o out # out is our lexical analyzer |
Yacc Design
Yacc Architecture Design

Similar to
Lex,Yaccis a tool for generatingy.tab.c, andy.tab.cis the syntax analyzer. As you can see, the input is theYaccsource program and the output isy.tab.c. In this program’s processing, theYacchandler is integrated intoYaccitself; the syntax analyzer is not made standalone, and only a predictive parsing table was used for simple testing.
Yacc Input File
1 |
|
For parsing convenience, this grammar differs slightly from the standard
Yaccgrammar, but this does not affect grammar parsing.
What is enclosed in
%{, %}is still the predefined output of the grammar; since we do not producey.tab.c, it serves only as a place holder here.%!, $!containtoken,head, andleft/right, all meaningful here:tokenrecords the terminals,headrecords the starting non-terminal (expr above), andleft, rightrecord precedence and associativity.
Yacc Output
- In this project
Yacchas no output; we added aparsefunction to theYaccclass, which reads the lexical analyzer’s generated file and produces the parsing output.
Lexical analyzer output file
1 | <$NUM,23> |
Output of the
parsefunction
1 | 0 s3 |
Processing Flow
- Now that we know the input and output of the
Yaccprogram, the flow description trims away irrelevant input/output and picks the main logic for introduction; for very specific details please read the code. Here we use the following grammar as an example for flow analysis:
1 | "S->S;A", |
- Storing the grammar and solving the
Firstsets
When the grammar is saved, it is first transformed: productions with the same non-terminal are merged, so the grammar above is stored like this:
1 | A -> E|i=E |
To solve the First sets, just call the makeFirst function.
1 | /** |
The pasted code above may not make the function’s usage easy to grasp; I will analyze it step by step below.
Let’s go through it in order. For A -> E|i=E, parsing A -> E gives First(E)∈First(A); the depth-first traversal will
solve First(E): First(E) = i, then First(A) = First(E) ∩ {i}.
Then for S->A|S;A, we know First(S)=First(A).
So First(A) = First(S) = First(E) = {i}.
- Constructing the LR(1) item set family
1 | /** |
Next I will explain the whole construction of the item set family starting from the initial state.
- Building the LR(1) initial state
In the items function, the expression #->.S, $ is first inserted as the start of the closure computation.
- Closure computation of the core productions
Taking the closure expansion of the initial state’s core production as an example, # -> .S term char: $ — expanding this expression
uses the LRState::findAllExpr function, which mainly does this: keep traversing the standard states; the function
mainly uses LRState::findAllExpr:
1 | /** |
- LR(1) item set family state transitions
When we reach some state and have performed the closure computation from the core productions, we next need to reach the next
state; at this point the items function calls LR1::getAllNextState to expand:
1 | /** |
- Building the ACTION and GOTO tables
After the LR(1) item set family is built, the LR1::makeACTIONGOTO function can be called to build the predictive parsing table.
During construction, ambiguous grammars may cause conflicts; here conflicts mainly appear
in building the Action table. The specific influence of precedence and associativity on table construction will not be expanded here — I believe
interested readers will explore the code.
1 | /** |
- Performing syntax analysis

Once we have the ACTION and GOTO tables, we need to apply them. Here we directly use the final output as the example.
This does not mean our LR1 only applies to the current input — it is only for convenience of explanation, and the computation
process in this example has been verified over a long time. The whole grammar is:
1 |
|

The header is ( ) * + - / NUM $

The header is A
Text to parse:
1 | <$NUM,23> |
When we read NUM we are in state 0; the corresponding action is s3 — a shift, and we are now in
state 3. Then we read +, and the action is r5 — reduce with production 5. Continuing this way, the stack diagram is below:
1 | symbol stack state stack actual computation stack current action |
Important Data Structure Definitions
1 | /** |
Using the Syntax Analyzer
- The concrete parsing process was listed above; next I mainly explain how to use the syntax analyzer:
1 | # use the lexical analysis tool to generate the file to parse |
Problems Encountered in the Project
- A fairly large project runs into many problems. Here I pick some of the perplexing problems encountered during the whole process, also as a reminder to myself to keep improving my programming skills to solve the problems met in future study and life.
Problems in Lex
- Converting infix regular expressions to postfix
Building the reverse Polish expression was our first step. Infix-to-postfix conversion had actually been done long before, but as the input of the whole program, its correctness had to be guaranteed first. Moreover, the postfix form of a regular expression differs slightly from that of an ordinary arithmetic expression, because an ordinary expression is joined by
+-*/, while a regular expression is joined by an invisible concatenation operator.Therefore, before writing code I first searched for similar problems, confirmed that others had written code already, and completed it only after referring to theirs. I believe this is unavoidable in project construction: sometimes, due to project requirements, we need to look at other people’s programs for reference beforehand.
- Defining the NFA
The NFA is the basis for converting to a DFA, and the representation of the NFA persisted all the way to the final construction of the lexical analyzer. Converting the postfix form of a regular expression to an NFA has, to some extent, a determinate form. Here we first proved that each NFA node needs at most two outgoing edges to represent all cases, including
a+, a*, a|b ... Actually this step is where the problem affecting the whole Lex construction lay: due to a small mistake of mine, the construction of NFAs of thea|bform went wrong throughout the whole NFA construction, affecting even the final composition of the lexical analyzer. I have to say such an annoying little error caused the failure of the entire system.
- Implementing the actions for regular expressions
Current program state: The previous program had actually already completed the conversion from NFA to DFA. But multiple NFAs have multiple end and initial states; converting NFAs to DFAs separately still cannot solve the analysis problem. Therefore, here we need to merge the NFAs first, and convert to a DFA after merging.
Problem: Ideas are always nice, but the problems that follow are not simple. Multiple NFAs have multiple end states, and the operation function corresponding to each end state differs. So although we merged the NFAs, the end-state information carried by each NFA could not be merged. This is the truly awkward moment: without solving this problem, the program cannot continue.
Solution: At this point I added an endFunc variable to the State and DState classes. Overall, adding the variable causes extra space usage and makes the program less elegant. But by then the main framework was already defined; the NFA and DFA creation classes could not be modified much more. When there are many states, the program’s memory footprint rises.
Since I pursued separating the DFA from the NFA as much as possible, and defined the State and DState classes separately, the resulting problem could only be solved by using extra space. This approach has pros and cons, but it is not a stopgap — it is a way of solving the problem.
- Representing regular expression states
Function input:
1 | ({letter}|_)({letter}|_|{digit})* { |
Here is a regular expression matching variable names (strings of letters, digits and underscores that may only start with a letter or underscore), together with the operation corresponding to the expression.
First, getReAndFunc is called to separate the regular expression from the processing logic; we get re and func corresponding to the two parts: re = ({letter}|)({letter}||{digit})* func = {…} The regular expression is then processed: because the expression contains long names like {letter} and {digit}, we use state2ch to perform the {letter} -> char substitution. The generated regular expression becomes: re_s = (a|)(a||b)* — here a and b are only filler characters; in the real substituted regex, a and b are invisible characters.
In addition: For a regular expression like (%=?), because the expression contains ‘‘, direct matching would produce errors, so we use % for escaping; the escape in a regular expression is *, which we also modify here, and it becomes: (*=?)
After this basic processing of the regular expression, we store the expression and its corresponding handler function into the NFA class: Re2NFA *pre2Nfa = new Re2NFA(re_s, func);
Problems in Yacc
- Representing productions
Everything is hard at the beginning. From the very start of building
Yacc, the problem of saving expressions could not be avoided, because constructing the LR1 state family requires building the First sets, so the initial choice of data structure matters. ChoosingExpressioncan save expressions likeS -> a | b, making it convenient to find the productions corresponding to the same non-terminal — but it is exactly such productions that caused trouble for the later LR1 construction. Since the expressions saved in theExpressionclass are mixed together, checking them at every state transition would be an enormous amount of work. Unable to bear this trouble, a new custom classSingleExpressionwas used when constructing the item set family; state transitions useS -> aB.c, $.
- The closure operation (expanding from the core productions)
In the whole expansion process, at first I only wanted the form from core productions to all productions, but in the end I discovered that during expansion we would encounter the end of a production (okay, I hadn’t considered that), and the subsequent parameter-tuning process was quite laborious.
1 | if (pos >= right.size()) { // if the current position is already at the end, do not process |
Problems Using CMake
- Building libraries with CMake
The project involves a large number of
.cpp, .hfiles; without a suitable management tool it is impossible to keep everyone’s progress in sync. TakingLexas an example:
1 | set(SOURCE_LIB ./lex.cpp ./lex.h |
- Adding input files
The build process needs some input files. Since
CMakebuilds out-of-source, theLexandYaccfiles must both be fed in; the following copies the files into the build directory.
1 | file(COPY |
Group Division of Labor
- This course design project consisted of three people:
09014310 Feng Panhe,09014312 Feng Yuhao, 09014315 Zhang Chunqiu
Lex
Infix to postfix for regular expressions, postfix to NFA, NFA to DFA,
construction of the Lex analysis program (reading the lex source file, generating the analyzer),
writing the lex source file, defining the lex source file specification
Yacc
Storing expressions and generating First sets, constructing the LR1 item set family, constructing the predictive parsing table, reading and parsing the Yacc source file, applying the parsing results (syntax analysis using the predictive parsing table)
References
Building your own Lex — a lexical analyzer generator in C++
Getting-started examples for lex and yacc
Regular expressions to NFA (C++)
Implementing Thompson’s construction in code: building NFA state machines from simple to complex
Lexical analysis (4) — NFA to DFA conversion, compiler principles