Yasir Explains/Compiler Design/Syntax Analysis/Parser
Syntax Analysis

Parser

On this page

What is a Parser?Input, Output, and the GrammarTypes of ParsersParser vs. Lexer — Division of Responsibility
Syntax Analysis

Parser

Understand what a parser does, how it sits between the lexer and the rest of the compiler, and what it produces — from a raw token stream to a structured parse tree.

What is a Parser?

After the lexer breaks source code into tokens, the parser takes over. Its job is to check whether those tokens form a grammatically valid program — and if so, to build a structured representation of that program.

Think of it this way:

Example
Source code: int x = a + b * 2;
Lexer output (token stream):
[int] [id:x] [=] [id:a] [+] [id:b] [*] [int:2] [;]

The parser transforms that token stream into a structured tree (AST):

AssignStmt
type: int
name: x
BinaryOp(+)
id: a
BinaryOp(*)
id: b
int: 2

The parser does two things:

  1. Syntax checking — reports errors if the token stream violates the grammar.
  2. Structure building — builds a parse tree (or AST) that captures the program's meaning.

Where the Parser Fits

Example
Source text
↓ [Lexer / Scanner]
Token stream
↓ [Parser / Syntax Analyser]
Parse tree (AST)
↓ [Semantic Analyser]
Annotated AST
↓ [Code Generator]
Machine code

The parser is the second phase of a compiler, called syntax analysis. It uses the grammar of the programming language (a context-free grammar) to check and understand the structure of the program.

Input, Output, and the Grammar

Input

The parser receives a token stream from the lexer. Each token has a type (keyword, identifier, operator, literal) and sometimes a value.

Example
Token stream for: a + b * c
Token 1: (ID, "a")
Token 2: (OP, "+")
Token 3: (ID, "b")
Token 4: (OP, "*")
Token 5: (ID, "c")

Grammar

The parser uses a context-free grammar (CFG) that defines the language's syntax. For example:

Example
E → E + T | T
T → T * F | F
F → ( E ) | id

This grammar tells the parser what sequences of tokens are valid expressions.

Output

The parser produces a parse tree (also called a concrete syntax tree) or an abstract syntax tree (AST):

Tree TypeDescription
Parse treeEvery grammar rule is a node — includes all terminals and non-terminals
ASTCompact form — only semantically important nodes are kept

Parse tree for "a + b * c":

E
E
T
F
a
+
T
T
F
b
*
F
c

The AST keeps only the essential structure — operator nodes become the tree, identifiers become leaves:

+
a
*
b
c

Types of Parsers

Parsers are classified by the direction in which they build the parse tree and the direction in which they scan the input.

All practical parsers scan input left to right (L). The difference is in how they build the tree:


Top-Down Parsers

Start from the start symbol (root) of the grammar and try to match the input by expanding non-terminals downward.

Example
Grammar: E → id + E | id
Input: id + id
Step 1: Start with E (the root)
Step 2: Try E → id + E → match "id", then "+", then expand E again
Step 3: Try E → id → match "id"
Done ✓

Top-down parsers build the tree from the top (root) to the bottom (leaves).

Examples:

  • Recursive Descent Parser — writes one function per non-terminal
  • Predictive Parser / LL(1) Parser — uses a parsing table to avoid backtracking

Bottom-Up Parsers

Start from the input tokens (leaves) and reduce them up to the start symbol.

Example
Input: id + id
Step 1: Shift "id" onto the stack
Step 2: Reduce: id → F → T → E
Step 3: Shift "+"
Step 4: Shift "id"
Step 5: Reduce: id → F → T
Step 6: Reduce: E + T → E
Done ✓

Bottom-up parsers build the tree from the leaves up to the root.

Examples:

  • LR Parser — handles a larger class of grammars than LL
  • LALR Parser — used in tools like yacc and bison

Comparison

FeatureTop-Down (LL)Bottom-Up (LR)
Tree constructionRoot → leavesLeaves → root
Grammar classLL(k) grammarsLR(k) grammars — larger class
Ease of implementationEasier (recursive descent)Needs parser generator tools
Error messagesUsually more readableCan be cryptic
Left recursionMust be eliminatedHandles it naturally

Parser vs. Lexer — Division of Responsibility

A common question is: why have two separate phases? Why not just have the parser handle everything?

The reason is separation of concerns — each phase handles a different class of language structure, and keeping them separate makes the compiler simpler and faster.

ConcernLexerParser
Formal modelRegular grammar / DFAContext-free grammar / PDA
HandlesTokens (identifiers, numbers, keywords)Statement and expression structure
Cannot handleNested structures, pairing✓ CFGs can handle nesting
SpeedO(n) — very fastO(n) for LL/LR, O(n³) general

What the lexer does not check:

The lexer sees tokens one at a time and has no memory of the past. It can recognise that ( and ) are valid parenthesis tokens, but it cannot check if they are balanced. That is the parser's job.

Example
Lexer accepts: ( ( ) ) ) ← valid individual tokens
Parser rejects: ( ( ) ) ) ← unbalanced parentheses, not in the grammar

What the parser does not check:

The parser checks syntax (structure), not semantics (meaning). It accepts grammatically correct code regardless of whether variables are declared or types match.

Example
Parser accepts: x = y + z ← syntactically valid
Semantic analysis rejects: if y and z are undeclared or have wrong types