Week 4 - Parser 1¶
In the previous chapter, we introduced the main components of a compiler's front end and explored lexical analysis. Next, we will turn to the parser. Before diving into the details, let us establish what a parser does: it takes the token stream produced by the lexer and applies a set of grammar rules to organize those tokens into a parse tree for further analysis. If the input cannot be parsed according to those rules, the program contains a syntax error.
What is a parser?
A Parser takes a token stream from a Lexer and turns it into a tree.
This tree is called a parse tree, and the rules used to build it form the grammar.
Recap: Context-Free Grammar (CFG)¶
We use a context-free grammar (CFG) to describe syntax rules. In the context of formal languages and compiler design, a grammar specifies the syntax of a language.
Like regular expressions, which we covered in the previous chapter, CFGs provide a formal way to describe a language. Regular expressions describe lexical rules: what individual tokens look like. CFGs describe syntax rules at a higher level: how those tokens fit together to form a structurally valid program. ANTLR uses context-free grammars to describe syntax.
Why are CFGs more expressive than regular expressions?
CFGs can describe structures that regular expressions cannot, including recursive and nested structures. A classic example is balanced parentheses: ().
Languages described by CFGs, known as context-free languages, are generally recognized using a pushdown automaton. Unlike the finite automaton used to recognize regular languages, a pushdown automaton has a stack, allowing it to handle recursive and nested structures.
A finite automaton can be viewed as a special case of a pushdown automaton. If you are interested, see Wikipedia - Automata theory.
Two essential components of a CFG are terminals and nonterminals.
Simply put, terminals are the tokens handed to the parser by the lexer. They are the basic units of the grammar and cannot be broken down any further within it. Nonterminals represent structures that the parser recognizes by combining tokens from the input stream. The rules for forming these structures are called productions. A production describes how terminals and nonterminals can form a nonterminal. Viewed in the other direction, it tells us which sequence of terminals and nonterminals a nonterminal can produce—hence the name "production."
Recap: Productions
In the lectures, we write a production as head -> body.
The head must be a nonterminal, while the body contains zero or more terminals or nonterminals.
Defining a CFG in ANTLR¶
Let us start with a simple ANTLR grammar:
grammar Calc1;
expr : expr ADD expr
| expr SUB expr
| factor
;
factor : INT
;
INT : [0-9]+ ;
ADD : '+' ;
SUB : '-' ;
WS : [ \t\r\n]+ -> skip ;
In ANTLR terminology, terminals are usually called tokens, and nonterminals are defined by rules.
Both definitions follow the same basic format: a name, a colon, the rule body, and a final semicolon. A token or rule definition can contain multiple alternatives.
ANTLR distinguishes lexer rules from parser rules by the first letter of their names: lexer rule names must begin with an uppercase letter, while parser rule names must begin with a lowercase letter. Lexer and parser rules can be mixed within the grammar; neither group has to come first.
The Calc1 grammar above contains:
- Four terminal definitions whose names begin with an uppercase letter.
- Two nonterminals whose names begin with a lowercase letter.
- Four productions, expressed as alternatives within the parser rules.
Parser Rule¶
Here is a basic parser rule:
The general format is ruleName : Alternatives ;.
A nonterminal can have multiple productions. Separate the alternatives with | and end the rule with ;:
Rule Elements¶
Each alternative can contain token references (starting with an uppercase letter), rule references (starting with a lowercase letter), or string literals such as 'literal'.
We can use string literals directly in a parser rule. ANTLR automatically creates and references a corresponding lexer rule, which you can see in the generated .tokens file.
For example, instead of defining ADD: '+', we can write the literal '+' directly in the parser rule:
Subrules¶
An alternative can itself contain a block of alternatives, called a subrule. You can think of a subrule as an anonymous rule defined in place.
Subrules have four forms, with syntax much like regular expressions:
(x | y | z): Match one of the alternatives exactly once.(x | y | z)?: Match one of the alternatives once, or not at all.(x | y | z)*: Match the block zero or more times, choosing an alternative each time.(x | y | z)+: Match the block one or more times, choosing an alternative each time.
Subrule Example: (...)* for Zero or More Repetitions¶
Consider this rule:
Here, ('+' NUMBER)* is a subrule. It means that the sequence '+' NUMBER can appear zero or more times.
✅ Matching Inputs¶
-
7- Zero repetitions of
('+' NUMBER).
- Zero repetitions of
-
7 + 2- One repetition.
-
7 + 2 + 5- Two repetitions.
-
7 + 2 + 5 + 8- Three repetitions.
❌ Non-Matching Inputs¶
-
+ 2- The rule must begin with a
NUMBER.
- The rule must begin with a
-
7 +- A
NUMBERis missing after the+.
- A
Using an ANTLR Parser¶
How do we use the parser generated by ANTLR? Let us revisit these lines from the example in Week 1 - Intro:
// Give ANTLR the string we want to parse
CharStream input = CharStreams.fromString("1 + 2");
// Create the lexer
Calc1Lexer lexer = new Calc1Lexer(input);
// Create the token stream
CommonTokenStream tokens = new CommonTokenStream(lexer);
// Create the parser
Calc1Parser parser = new Calc1Parser(tokens);
// Start parsing at the 'expr' rule
ParseTree tree = parser.expr();
// Print the tree
System.out.println(tree.toStringTree(parser));
When we create the Calc1Parser, we pass it the CommonTokenStream produced in the previous step. Calling parser.expr() then asks the parser to build a parse tree rooted at expr from that token stream.
ANTLR generates a method for every parser rule, using the rule's name as the method name. Calling one of these methods starts parsing from that rule, which becomes the root of the resulting tree.
Start Rules and EOF¶
The rule we choose when we first invoke the parser is called the start rule. In ParseTree tree = parser.expr(), the start rule is expr, which is also the nonterminal at the root of the tree we want to build. Keep in mind that a start rule does not necessarily consume every token in the stream: it consumes only the tokens needed to build that parse tree.
This is why running the program above can produce the error line 1:5 no viable alternative at input '<EOF>': after the parser consumes the input tokens, it encounters the special token <EOF>, which marks the end of the stream. If it cannot find a valid alternative at that point, it reports this error.
Usually, we want the parser to process an entire source file. To do so, we can define a higher-level start rule that explicitly includes the end-of-file token:
Then use ParseTree tree = parser.root(); in Java to parse the entire token stream.
Quickly Inspecting a Parse Tree¶
By now, this command should look familiar:
We have been using it to generate lexers and parsers since the earlier labs.
There is no ANTLR magic built into make itself, though. It simply runs the commands defined in the Makefile at the project root.
What Does make Actually Do?¶
In the Makefile we have been using, this is the command that generates the ANTLR code:
java -jar $(ANTLR_JAR) \
-visitor \
-o $(OUTPUT_DIR)/$(basename $<) \
-package generated.$(basename $<) \
$<
We have already specified the location of ANTLR:
For a grammar named Calc1.g4, the Makefile rule therefore runs a command like this:
java -jar libs/antlr-4.13.2-complete.jar \
-visitor \
-o src/main/java/generated/Calc1 \
-package generated.Calc1 \
Calc1.g4
Think of make as a handy command runner
It finds our .g4 files and invokes:
to generate the corresponding Java lexer, parser, and visitor.
ANTLR is doing the actual code generation.
We Have Already Printed a Parse Tree¶
In Week 1 - Intro, we used this line to inspect the parse tree produced by the parser:
Here, tree is the ParseTree obtained by parsing from a particular parser rule. The call:
converts that tree into a parenthesized representation that is easy to display in a terminal.
For example, given this input:
we might get output like this:
Recall that producing this tree required us to set up the entire parsing pipeline ourselves:
In other words, we manually created the lexer, token stream, and parser in Main.java, then printed the resulting parse tree.
The antlr-4.13.2-complete.jar file also includes a ready-made testing tool:
It provides a quick way to carry out much the same process.
There is nothing mysterious about TestRig
Previously, we created the lexer, token stream, and parser ourselves in Main.java, then called:
TestRig packages that testing workflow into a tool we can run directly.
If we just want to see how a grammar parses a piece of input, there is no need to edit Main.java every time.
Using -tree to Inspect a Parse Tree¶
Suppose we have a grammar file named:
with generated Java classes in the package:
and we want to start parsing from this rule:
First, run the usual command:
and make sure the generated Java source files have been compiled.
Then run:
echo '1 + 2 * 3' | \
java -cp "bin:libs/antlr-4.13.2-complete.jar" \
org.antlr.v4.gui.TestRig \
generated.Calc1.Calc1 \
expr \
-tree
ANTLR will print the parse tree directly in the terminal. For example:
The exact structure depends on your grammar.
Although this representation does not look like a tree drawn on paper, the parentheses show how the nodes are nested.
For example, we can take:
and expand it over multiple lines:
This makes the parent-child relationships easier to see.
What Does This Command Mean?¶
The command may look long, but there are only a few parts to focus on:
| Argument | Meaning |
|---|---|
bin:libs/antlr-4.13.2-complete.jar |
Where the Java runtime looks for compiled .class files and ANTLR classes |
org.antlr.v4.gui.TestRig |
The grammar testing tool bundled with ANTLR |
generated.Calc1.Calc1 |
The grammar to use |
expr |
The parser rule to start from |
-tree |
Print the parse tree in the terminal |
The parts you will change most often are:
For example, if your grammar is:
its generated Java package is:
and its entry rule is:
then you can use:
echo '...' | \
java -cp "bin:libs/antlr-4.13.2-complete.jar" \
org.antlr.v4.gui.TestRig \
generated.SPL.SPL \
compUnit \
-tree
Look at the tree when debugging your parser
When you are unsure whether your grammar behaves as intended, -tree is a useful debugging tool.
It is especially helpful for checking:
- Whether a piece of input was parsed correctly.
- What structure a particular parser rule matched.
- Whether operator precedence and associativity work as expected.
- Why an expression was parsed differently from what you had in mind.
- How a grammar change affects the parse tree.
Looking at the tree ANTLR actually produces can be much more helpful than repeatedly trying to work it out from the .g4 file alone.
Viewing a Graphical Parse Tree¶
TestRig also provides the option:
which displays the parse tree graphically.
For example:
echo '1 + 2 * 3' | \
java -cp "bin:libs/antlr-4.13.2-complete.jar" \
org.antlr.v4.gui.TestRig \
generated.Calc1.Calc1 \
expr \
-gui
If a graphical display is available and configured correctly, ANTLR opens a window showing the tree.
However, we usually develop on a virtual machine through SSH or VS Code Remote-SSH.
In that setup, ANTLR runs on the remote virtual machine. The remote terminal may not have access to a graphical display by default, so -gui may not be able to open a window directly.
Explore this further if you are interested
It is possible to display GUI applications from a remote Linux environment.
If you would like to learn more, look into:
- X11 forwarding.
- The
DISPLAYenvironment variable. - How remote Linux applications display their windows on a local computer.
These topics are outside the course requirements, and you do not need them to complete the projects.
For everyday parser debugging in this course, using:
is sufficient.
First, you need a tree to inspect
Both -tree and -gui display a parse tree, so you need a parser before you can use them for this purpose.
If your .g4 file begins with:
then you currently have only a lexer. It produces a token stream, not a parse tree.
Without a parser, there is no parse tree to display.
Exercise¶
Extend the Calc1 grammar introduced above to support multiplication, division, and parentheses in addition to addition and subtraction. Then write a small program to test it with the following input: