A. K. M NIHALUL KABIR <akm.nihalul.kabir@g.bracu.ac.bd>
420 theory [MID]
Nihal Kabir <nihalspeaking@gmail.com>
To: akm.nihalul.kabir@g.bracu.ac.bd
Thu, May 8, 2025 at 6:47 PM
1. The Structure and Stages of the Compiler
A compiler is a program that translates source code written in a high-level programming language (like
C++, Java, Python) into a low-level language (like assembly language or machine code) that a computer
can execute directly. This translation process is complex and is typically broken down into several distinct
phases or stages, often grouped into a frontend and a backend.
Phases of a Compiler:
1. Lexical Analysis (Scanning):
Input: Source code character stream.
Output: Stream of tokens.
Function: Reads the source program character by character and groups them into meaningful
sequences called lexemes. For each lexeme, the lexical analyzer produces1 a token representing
it (e.g., <identifier, "count"> , <keyword, "while"> , <operator, "+"> ). It also typically
removes whitespace and comments.
2. Syntax Analysis (Parsing):
Input: Stream of tokens.
Output: Parse tree or Abstract Syntax Tree (AST).
Function: Takes the token stream and checks if it conforms to the grammatical rules (syntax) of
the source language, usually defined by a Context-Free Grammar. It organizes the tokens into a
hierarchical structure, often a parse tree, which represents the syntactic structure of the source
code.
3. Semantic Analysis:
Input: Parse tree/AST.
Output: Annotated parse tree/AST.
Function: Checks the parse tree for semantic correctness, ensuring the code makes sense
according to the language's rules beyond just syntax. This includes:
Type checking (e.g., ensuring you don't add a string to an integer without conversion).
Scope resolution (e.g., checking if variables are declared before use).
Matching function arguments with parameters.
4. Intermediate Code Generation:
Input: Annotated parse tree/AST.
Output: Intermediate Representation (IR).
Function: Generates an explicit low-level or machine-like intermediate representation of the
source program. This IR should be easy to produce from the source and easy to translate into
the target machine code. Common forms include three-address code (e.g., t1 = a + b ),
abstract machine code (like Java bytecode), or stack-based code.
5. Code Optimization:
Input: Intermediate Representation.
Output: Optimized Intermediate Representation.
Function: Attempts to improve the intermediate code to make the resulting target code run
faster, use less memory, or consume less power. Optimization techniques include constant
folding, common subexpression elimination, loop optimizations, dead code elimination, etc. This
phase is optional but crucial for performance.
6. Target Code Generation:
Input: Optimized Intermediate Representation.
Output: Target language code (e.g., assembly or machine code).
Function: Translates the optimized intermediate code into the target language. This involves
register allocation, instruction selection, and generating the final code sequences.
Supporting Components:
Symbol Table Management: A data structure used throughout compilation to store information about
identifiers (variables, functions, types, etc.) encountered in the source code, such as their type,
scope, and memory location. All phases interact with the symbol table.
Error Handling: Detects errors during each phase, reports them to the user in an understandable way
(e.g., line number, error message), and potentially attempts error recovery to continue compilation
and find more errors.
2. The Analysis-Synthesis Model of Compilation
This is a high-level way to view the compilation process, dividing it into two major parts:
1. Analysis (Frontend):
Phases: Lexical Analysis, Syntax Analysis, Semantic Analysis, (sometimes Intermediate Code
Generation).
Purpose: Breaks down the source program into its constituent parts and imposes a grammatical
structure. It creates an intermediate representation of the source program and gathers
information about it in the symbol table. It performs extensive error checking related to syntax
and semantics. The frontend is often dependent on the source language but largely independent
of the target machine architecture.
2. Synthesis (Backend):
Phases: (sometimes Intermediate Code Generation), Code Optimization, Target Code
Generation.
Purpose: Constructs the desired target program from the intermediate representation and the
information in the symbol table. The backend2 is often dependent on the target machine
architecture but largely independent of the source language.
This model allows for modularity. For example, you could potentially reuse the same backend for
compilers of different source languages (targeting the same machine) or reuse the same frontend with
different backends to target different machines.
3. Difference Between a Compiler and an Interpreter
Both compilers and interpreters are translators that execute programs written in a high-level language,
but they work differently:
Feature
Compiler
Interpreter
Process
Translates the entire program at
once before execution.
Translates and executes the program line by line
or statement by statement.
Input
Entire source code file.
Source code statement/unit.
Output
Executable target code (e.g.,
machine code).
Immediate actions/results (no separate
executable file).
Execution
Separate compilation and
execution steps.
Translation and execution are interleaved.
Speed
Compiled code generally runs
faster.
Interpretation is generally slower due to runtime
translation overhead.
Error
Check
Reports syntax/static semantic
errors after scanning the whole
program (compile-time errors).
Reports errors as they are encountered during
execution (runtime errors). Can be harder to
debug overall structure.
Memory
Generates an intermediate object
code which requires linking and
more memory overall.
Typically uses memory more efficiently during
execution (no object code).
Platform
Target code is often platformspecific.
Interpreters are often more portable (write
once, run anywhere if the interpreter exists).
Debugging
Can be harder to debug the
compiled code.
Easier to debug line-by-line, good for rapid
development/prototyping.
Examples
C, C++, Fortran, Rust
Python (standard CPython), Ruby, BASIC,
JavaScript (often JIT compiled now)
Note: Many modern languages (like Java, C#, Python with PyPy) use a hybrid approach, compiling source
code to an intermediate bytecode, which is then interpreted or Just-In-Time (JIT) compiled to native
machine code by a virtual machine.
4. The Role of a Lexical Analyzer (Scanner)
The Lexical Analyzer is the first phase of the compiler. Its main tasks are:
1. Read Input Characters: It reads the source code character by character from left to right.
2. Group into Lexemes: It groups sequences of characters into meaningful units called lexemes.
3. Produce Tokens: For each lexeme identified, it produces a token and passes it to the next phase
(Syntax Analyzer). A token typically consists of a token name (an abstract category) and an optional
attribute value (specific information about the lexeme).
4. Discard Whitespace and Comments: It usually ignores or removes whitespace (spaces, tabs,
newlines) and comments, as they are typically irrelevant to the program's structure.
5. Track Line Numbers: It often keeps track of line numbers to help with error reporting.
6. Interact with Symbol Table: It may insert identifiers found into the symbol table or retrieve
information about them (though detailed symbol table interaction is often deferred to later phases).
Example:
Source code: position = initial + rate * 60;
Tokens produced might be:
<id, pointer_to_symbol_table_entry_for_"position">
<assign_op, >
<id, pointer_to_symbol_table_entry_for_"initial">
<add_op, >
<id, pointer_to_symbol_table_entry_for_"rate">
<mult_op, >
<number, 60>
<semicolon, >
5. Tokens, Patterns, Token Attributes
Token: An abstract symbol representing a kind of lexical unit. It's the output of the lexical analyzer
and the input to the parser. It's often represented as a pair: <token-name, attribute-value> .
Token Name: The abstract category (e.g., identifier , keyword , integer_literal , operator ,
delimiter ).
Attribute Value: Optional additional information about the specific lexeme matched. This could be
a pointer to the symbol table entry for an identifier, the numerical value for a number, or the
specific operator symbol. For keywords or single-character delimiters, the attribute might be
omitted or implied by the token name.
Lexeme: The actual sequence of characters in the source code that matches a pattern for a token.
(e.g., count , while , 123 , + , ; ).
Pattern: A rule that describes the set of lexemes that can represent a particular token. Patterns are
commonly specified using regular expressions.
Example Pattern for identifier : letter (letter | digit)* (A letter followed by zero or more
letters or digits)
Example Pattern for integer_literal : digit+ (One or more digits)
Example Pattern for keyword : "if" | "else" | "while" (Specific reserved words)
6. Regular Definitions
Regular definitions are a shorthand notation used to define complex regular expressions based on simpler
ones. They allow assigning names to regular expressions, making patterns easier to write, read, and
reuse.
A regular definition is a sequence of definitions of the form:
d1 → r1
d2 → r2
...
dn → rn
Where each di is a distinct name, and each ri is a regular expression over the alphabet
Σ ∪ {d1, d2, ..., di−1} (symbols from the input alphabet or previously defined names).
Example: Defining identifiers and numbers:
letter_ -> [A-Za-z_] (Using _ to avoid conflict with the definition name letter )
digit -> [0-9]
id -> letter_ ( letter_ | digit )*
number -> digit+ ( . digit+ )? ( E [+-]? digit+ )? (Handles integers, decimals, and scientific
notation)
These definitions simplify the specification of token patterns for the lexical analyzer generator.
7. The Structure of a Generated Lexical Analyzer
Lexical analyzers are often automatically generated by tools like Lex or Flex based on specifications
(regular definitions and actions). The generated analyzer typically has the following structure:
1. Finite Automaton (FA): The core is often a Deterministic Finite Automaton (DFA) derived from the
regular expressions defining the tokens. This DFA recognizes the lexemes.
States: Represent the progress in matching a pattern.
Transitions: Based on the next input character, the DFA moves from one state to another.
Accepting States: Indicate that a complete lexeme matching a pattern has been found. Different
accepting states may correspond to different token types.
2. Input Buffer: Stores the source code characters, often using techniques like double buffering and
sentinels for efficiency and lookahead.
3. Lexeme Pointers: lexemeBegin and forward pointers track the start and current position within the
input buffer corresponding to the potential lexeme being scanned.
4. Driver Routine: Orchestrates the process. It calls the DFA simulation repeatedly to find the next
lexeme.
5. DFA Simulator: Takes the current state and the next input character to determine the next state
based on the transition table/function of the DFA.
6. Action Routines: Code associated with accepting states. When the DFA reaches an accepting state:
It determines the longest possible match.
It executes the corresponding action (e.g., return a specific token, perhaps storing the lexeme or
its value as an attribute, interact with the symbol table).
It resets the lexemeBegin pointer for the next scan.
7. Lookahead: The analyzer might need to read one or more characters beyond the end of the current
lexeme to decide where it ends or which pattern it matches (e.g., distinguishing > from >= ). The
FA structure handles this.
8. The Role of a Parser (Syntax Analyzer)
The Parser is the second phase of the compiler. Its primary roles are:
1. Verify Syntax: Checks if the stream of tokens produced by the lexical analyzer conforms to the
language's grammatical rules (syntax). These rules are typically specified using a Context-Free
Grammar (CFG).
2. Impose Structure: If the token stream is syntactically valid, the parser builds a hierarchical structure
that represents how the tokens relate according to the grammar. This structure is usually a Parse
Tree or an Abstract Syntax Tree (AST).
3. Report Syntax Errors: If the token stream violates the grammar rules, the parser must detect this,
report a meaningful error message, and potentially attempt error recovery to continue parsing and
find more errors.
4. Foundation for Later Phases: The structure created by the parser (the parse tree or AST) becomes
the input for subsequent phases like Semantic Analysis and Intermediate Code Generation.
Essentially, the parser determines if the sequence of tokens makes sense structurally, like ensuring
parentheses are balanced, expressions are formed correctly, and statements follow the language's
specified format.
9. Context-Free Grammars (CFGs)
A Context-Free Grammar (CFG) is a formal way to describe the syntax of a language. It defines the rules
for how symbols (tokens) can be combined to form valid program structures. A CFG is defined by a 4tuple G = (V , T , P , S):
1. V (Non-terminals or Variables): A finite set of symbols that represent syntactic categories or
structures (e.g., statement , expression , declaration ). They appear on the left-hand side of
productions.
2. T (Terminals): A finite set of symbols corresponding to the tokens produced by the lexical analyzer
(e.g., id , number , + , - , ; , if , while ). They are the basic symbols from which strings in the
language are formed. V and T are disjoint (V ∩ T = ∅).
3. P (Productions or Rules): A finite set of rules of the form A → α, where:
A ∈ V (A is a single non-terminal).
α ∈ (V ∪ T )∗ (alpha is a sequence of zero or more terminals and non-terminals).
This rule means that the non-terminal A can be replaced by the sequence α.
4. S (Start Symbol): A special non-terminal S ∈ V that represents the top-level structure of the
language (e.g., program ). Derivations start from this symbol.
Example Grammar (Simple Arithmetic Expressions):
G=(V,T,P,E)
V={E,T,F} (Expression, Term, Factor)
T={id,number,+,∗,(,)}
S=E
P:
1. E → E + T
2. E → T
3. T → T ∗ F
4. T → F
5. F → (E)
6. F → id
7. F → number
This grammar defines expressions involving addition, multiplication, parentheses, identifiers, and
numbers.
10. Parse Trees and Derivations
Derivation: A sequence of steps starting from the start symbol S, where each step applies a
production rule to replace a non-terminal with the right-hand side (RHS) of one of its productions.
This process continues until only terminal symbols remain. The resulting sequence of terminals is a
string derived from the grammar, belonging to the language L(G).
Leftmost Derivation: At each step, the leftmost non-terminal in the current sequence is chosen
for replacement.
Rightmost Derivation: At each step, the rightmost non-terminal is chosen for replacement.
(Crucial for bottom-up parsing).
Example (Deriving id * number + id using the grammar above, Rightmost Derivation):
E⇒E+T (Rule 1)
⇒E+T (Rule 2)
⇒E+T∗F (Rule 3)
⇒E+T∗id (Rule 6, but this isn't rightmost... let's retry properly)
Example (Rightmost Derivation of id + id * number):
E⇒E+T (Rule 1)
⇒E+T∗F (Rule 3)
⇒E+T∗number (Rule 7)
⇒E+F∗number (Rule 4)
⇒E+id∗number (Rule 6)
⇒T+id∗number (Rule 2)
⇒F+id∗number (Rule 4)
⇒id+id∗number (Rule 6)
Parse Tree: A graphical representation of a derivation. It shows how the start symbol derives a string
of terminals.
The root is labeled with the start symbol S.
Each leaf node is labeled with a terminal symbol or the empty string ε.
Each internal node is labeled with a non-terminal symbol A.
If an internal node is labeled A and its children are labeled X1, X2, ..., Xk from left to right, then
A → X1X2...Xk must be a production in P .
The parse tree visually depicts the syntactic structure according to the grammar. The sequence of
leaves read from left to right forms the derived string (the yield of the tree). For any given string in
the language, there might be one or more parse trees.
11. Grammar Ambiguity and Mitigation Techniques
Ambiguity: A grammar G is ambiguous if there exists at least one string w ∈ L(G) for which there are
two or more distinct parse trees (or equivalently, two or more distinct leftmost derivations, or two or
more distinct rightmost derivations).
Problem: Ambiguity is usually undesirable in programming language grammars because it means
a single program construct could have multiple interpretations (syntactic structures), potentially
leading to different semantics or execution results.
Classic Example: The "dangling else" problem. Consider: stmt -> if expr then stmt stmt -> if
expr then stmt else stmt stmt -> other_stmt For the input if E1 then if E2 then S1 else S2 ,
the else S2 could potentially attach to the first if or the second if , leading to two different
parse trees.
Another Example: The expression grammar above is ambiguous because id + id * number could
be parsed as (id + id) * number or id + (id * number) without precedence rules.
Mitigation Techniques:
1. Rewrite the Grammar: Often, an ambiguous grammar can be rewritten into an equivalent
unambiguous one that enforces the desired structure.
Precedence: Introduce different non-terminals for different precedence levels (e.g., Term for
* , Expression for + ). The example grammar above already does this.
Associativity: Use left-recursion for left-associative operators ( E -> E + T | T ) and rightrecursion for right-associative operators ( A -> B = A | B ).
Dangling Else: Rewrite the grammar to enforce that an else always matches the nearest
unmatched then . (e.g., by distinguishing 'matched' statements ending in else from
'open' statements not ending in else ).
2. Disambiguating Rules (Parser Generator): Parser generator tools (like Yacc, Bison) allow
specifying rules outside the grammar itself to resolve ambiguities:
Precedence and Associativity Declarations: ( %left '+', '-', %left '*', '/' ) declare that
* and / have higher precedence than + and - , and that all are left-associative. This
resolves the expression ambiguity without changing the grammar productions directly.
Default Resolutions: Shift-reduce parsers often default to resolving shift-reduce conflicts in
favor of shifting, which usually handles the dangling else correctly.
12. Verifying the Language Generated by a Grammar
The language generated by a grammar G, denoted L(G), is the set of all strings of terminals that can be
derived from the start symbol S.
L(G)={w∈T∗∣S⇒∗w}
where ⇒∗ means "derives in zero or more steps".
Verifying the entire language L(G) usually means proving properties about the set of strings generated.
This can involve:
1. Mathematical Proof: Using induction on the length of derivations or the structure of the grammar to
prove that all strings with a certain property are generated, and that only strings with that property
are generated.
2. Equivalence: Showing that the grammar generates the same language as another known grammar or
a regular expression (if the language is regular).
3. Pumping Lemmas: (Primarily used to prove a language cannot be generated by a certain type of
grammar, e.g., the pumping lemma for context-free languages can show a language is not contextfree).
However, in the context of compilation, "verifying the language generated by a grammar" more commonly
refers to the process of parsing: determining if a specific input string w belongs to L(G). The parser
effectively performs this verification. If the parser can successfully construct a parse tree for w starting
from S, then w ∈ L(G). If the parser fails and reports a syntax error, then w ∈/ L(G).
13. Concept of Reductions (Bottom-Up Parsing)
Bottom-up parsing works by constructing the parse tree from the leaves (the input string) up towards the
root (the start symbol). It attempts to "reduce" a string w back to the start symbol S.
A reduction is the reverse of a step in a rightmost derivation. It involves:
1. Identifying a Handle: Finding a substring on the top of the parsing stack that matches the right-hand
side (β) of some production rule (A → β). This substring is called a "handle".
2. Replacing: Replacing that substring (β) on the stack with the non-terminal on the left-hand side (A)
of the production.
The goal is to apply reductions repeatedly, effectively tracing a rightmost derivation in reverse, until the
stack contains only the start symbol S and the input is empty.
14. Stack Simulation Example of Bottom-up Parsing
Let's use a simplified shift-reduce parser idea for the grammar:
1. E → E + T
2. E → T
3. T → T ∗ F
4. T → F
5. F → (E)
6. F → id
And the input string: id * id + id $ ( $ is the end-of-input marker)
Stack
Input
Action
Rule Used
$
id * id + id $
Shift
$ id
* id + id $
Reduce ( id is handle for F)
F -> id (6)
$F
* id + id $
Reduce ( F is handle for T)
T -> F (4)
$T
* id + id $
Shift
$T*
id + id $
Shift
$ T * id
+ id $
Reduce ( id is handle for F)
F -> id (6)
$T*F
+ id $
Reduce ( T*F is handle for T)
T -> T*F(3)
$T
+ id $
Reduce ( T is handle for E)
E -> T (2)
$E
+ id $
Shift
$E+
id $
Shift
$ E + id
$
Reduce ( id is handle for F)
F -> id (6)
$E+F
$
Reduce ( F is handle for T)
T -> F (4)
$E+T
$
Reduce ( E+T is handle for E)
E -> E+T(1)
$E
$
Accept
Explanation:
The parser maintains a stack (initially containing $ ) and reads the input tokens.
Shift: Moves the next input token onto the stack.
Reduce: Finds a sequence of symbols β on top of the stack that matches the RHS of a production
A → β. It pops β and pushes A. The key challenge is deciding when to shift and when (and by which
rule) to reduce. This decision logic is embedded in LR parsing tables.
Accept: When the stack contains $ S (start symbol) and the input is $ , parsing is successful.
Error: If no valid action can be taken.
15. Concept of Shift-Reduce Parsing
Shift-reduce parsing is the most common technique for bottom-up parsing. As seen in the example, the
parser performs two fundamental actions:
1. Shift: Moves the next input token from the input buffer onto the top of the parser's stack.
2. Reduce: Finds a handle (a sequence of symbols β on top of the stack matching the RHS of a
production A → β) and replaces it with the corresponding non-terminal A. This corresponds to
building a node in the parse tree.
The core difficulty lies in the decision-making process: at any point, should the parser shift the next token,
or is there a handle on top of the stack that should be reduced? And if a reduction is possible, which
production rule should be used if multiple RHS match the stack top? LR parsers use lookahead tokens and
precomputed parsing tables to make these decisions deterministically.
16. Handles and Handle Pruning
Handle: Formally, a handle is a substring β that matches the right-hand side of a production A → β,
found at a specific position within a right-sentential form (a string derivable from S using a rightmost
derivation), such that replacing β with A produces the previous right-sentential form in the derivation.
In the context of shift-reduce parsing, a handle is a sequence of symbols β on top of the stack
that matches the RHS of a production A → β, and whose reduction represents one step back in
the reverse of a rightmost derivation. It's the correct RHS to reduce at that point to eventually
reach the start symbol.
Handle Pruning: The process performed by a shift-reduce parser. It repeatedly identifies and reduces
handles on the stack. Each reduction "prunes" the handle (RHS) and replaces it with the non-terminal
(LHS), effectively moving one step up in the parse tree construction or one step backward in a
rightmost derivation. This continues until the start symbol is reached.
Finding the handle is the crucial part, guided by the parsing table in LR parsers.
17. LR(0) Items
LR parsers (including LR(0), SLR, LALR, LR(1)) use the concept of "items" to track the parsing progress.
An LR(0) item for a grammar G is a production rule from G with a dot ( . ) placed somewhere on the
right-hand side.
Meaning: An item [A -> α . β] indicates that we might currently be parsing a section of the input
corresponding to a derivation from A. We have already seen input corresponding to the derivation of
α, and we expect to see input corresponding to the derivation of β next.
Examples (from the expression grammar):
[E -> . E + T] (We expect to see an E next)
[E -> E . + T] (We have seen an E, expect a '+' next)
[E -> E + . T] (We have seen 'E +', expect a T next)
[E -> E + T .] (We have seen 'E + T', the rule is complete) - This is a reduce item.
[T -> .] (If there was a rule T -> ε, representing we expect to see nothing for T)
The set of LR(0) items forms the basis for constructing the states of the LR parser's control automaton.
18. Closure of a LR(0) Item Set
The Closure operation computes the complete set of items that should be considered together in a single
parser state. It captures the idea that if we expect to see a non-terminal B next (as indicated by an item
like [A -> α . B β] ), we must also be prepared for any production that B itself might start with.
Closure(I) Algorithm: (where I is a set of items)
1. Initialize Closure(I) = I .
2. Repeat until no more items can be added:
For each item [A -> α . B β] in Closure(I) , where B is a non-terminal:
For each production B -> γ in the grammar:
Add the item [B -> . γ] to Closure(I) if it's not already there.
3. Return Closure(I) .
Example: Grammar:
1. S′ → E
2. E → E + T
3. E → T
4. T → id
Let I={[S′→.E]}.
Compute Closure(I):
Start with Closure(I) = { [S' -> . E] } .
Item [S' -> . E] has . E . Non-terminal is E.
Productions for E are E -> E + T and E -> T . Add [E -> . E + T] and [E -> . T] to the set.
Closure(I) = { [S' -> . E], [E -> . E + T], [E -> . T] } .
Now consider [E -> . E + T] . It has . E . Add E's productions again (already there).
Consider [E -> . T] . It has . T . Non-terminal is T.
Production for T is T -> id . Add [T -> . id] . Closure(I) = { [S' -> . E], [E -> . E + T], [E -> .
T], [T -> . id] } .
Consider [T -> . id] . It has . id . id is a terminal, so no additions.
No more items can be added. This final set is Closure({[S' -> . E]}) . This set represents the initial
state (State 0) of the LR(0) automaton.
19. LR(0) Automaton Construction
The LR(0) automaton (also called the Canonical Collection of LR(0) Item Sets) represents the states and
transitions of an LR(0) parser. Each state corresponds to a set of LR(0) items generated by the Closure
operation.
Algorithm:
1. Augment Grammar: Add a new start symbol S′ and a production S′ → S, where S is the original start
symbol. This ensures the final accept state is unique.
2. Initial State (I0): Compute Closure({[S' -> . S]}) . This set is the initial state I0.
3. Compute States and Transitions: Maintain a collection of item sets (states) found so far, initially just
{I0}. Repeat until no new states can be added:
Select an unprocessed state (item set) I from the collection.
For each grammar symbol X (terminal or non-terminal):
Compute the GOTO(I, X) set: This is the closure of all items [A -> α X . β] such that [A > α . X β] is in I.
If GOTO(I, X) is not empty and is not already in the collection of states:
Add this new set J = GOTO(I, X) to the collection as a new state.
Add a transition from state I to state J labeled with symbol X.
4. The resulting collection of item sets (states) and the GOTO transitions form the LR(0) automaton.
20. FIRST and FOLLOW Computation
These sets are crucial for constructing SLR, LALR, and LR(1) parsing tables. They help predict which
tokens can appear in different contexts.
FIRST(α): The set of terminals that can appear as the first symbol in a string derived from the
sequence of grammar symbols α. If α can derive the empty string ϵ, then ϵ is also included in
FIRST(α).
Computation Rules (Simplified):
FIRST(a) = {a} if a is a terminal.
FIRST(X) : If X -> Y1 Y2 ... Yk is a production:
Add FIRST(Y1) (excluding ε) to FIRST(X) .
If ε is in FIRST(Y1) , add FIRST(Y2) (excluding ε) to FIRST(X) .
Continue this: If ε is in FIRST(Y1)...FIRST(Yi-1) , add FIRST(Yi) (excluding ε).
If ε is in FIRST(Yj) for all j=1..k , then add ε to FIRST(X) .
If X -> ε is a production, add ε to FIRST(X) .
Apply these rules iteratively until no FIRST sets change.
FIRST(αβ) = FIRST(α) (excluding ε) ∪ (if ε ∈ FIRST(α) then FIRST(β) else ∅).
FOLLOW(A): The set of terminals (including the end-marker $ ) that can appear immediately after
the non-terminal A in some sentential form (a string derived from the start symbol S′, potentially
containing non-terminals).
Computation Rules (Simplified):
1. Place $ (end-marker) in FOLLOW(S') (where S' is the augmented start symbol).
2. If there is a production B -> α A β :
Add everything in FIRST(β) (except ε) to FOLLOW(A) .
If FIRST(β) contains ε (i.e., β can derive ε), then add everything in FOLLOW(B) to
FOLLOW(A) .
3. If there is a production B -> α A :
Add everything in FOLLOW(B) to FOLLOW(A) .
4. Apply these rules iteratively until no FOLLOW sets change.
21. Generation of SLR Parsing Table from LR(0) Automaton and FOLLOW Set
SLR (Simple LR) parsing uses the LR(0) automaton states but employs FOLLOW sets to help decide on
reduce actions, making it more powerful than pure LR(0).
Input: LR(0) automaton states (I0,I1,...,In), GOTO function, FOLLOW sets for all non-terminals.
Output: SLR Parsing Table (ACTION and GOTO parts).
Table Construction Algorithm:
For each state Ii in the automaton:
1. ACTION Table Entries (indexed by state i and terminal a ):
Shift: If item [A -> α . a β] is in Ii (where a is a terminal) and GOTO(I_i, a) = I_j , then set
ACTION[i, a] = "shift j" .
Reduce: If item [A -> α .] (a complete item, where A
in FOLLOW(A), set ACTION[i, b] = "reduce A -> α" .
= S′) is in Ii, then for all terminals b
Accept: If item [S' -> S .] is in Ii, then set ACTION[i, $] = "accept" .
Error: All other entries in ACTION are "error" (usually left blank).
2. GOTO Table Entries (indexed by state i and non-terminal A ):
If GOTO(I_i, A) = I_j , then set GOTO[i, A] = j .
All other entries in GOTO are "error".
Key difference from LR(0): The reduce action reduce A -> α in state i is only placed in ACTION[i, b] if
the lookahead terminal b is in FOLLOW(A) . This significantly reduces conflicts compared to LR(0) (which
would place the reduce action for all terminals).
22. Behavior of an SLR Parser
An SLR parser operates using:
1. A Stack: Stores parser states (represented by their numbers, e.g., 0, 1, 2...). Initially contains state
0.
2. An Input Buffer: Contains the sequence of tokens from the lexical analyzer, ending with $ .
3. The SLR Parsing Table (ACTION and GOTO).
Algorithm:
1. Initialize stack with state 0. Let a be the first token in the input buffer.
2. Repeat forever:
Let s be the state currently on top of the stack.
Consult the parsing table entry ACTION[s, a] .
If ACTION[s, a] == "shift j" :
Push state j onto the stack.
Advance the input pointer to the next token (let a be the new token).
If ACTION[s, a] == "reduce A -> α" :
Let the production be rule number k . (Output k if generating parse steps).
Pop ∣α∣ symbols (states) from the stack (where ∣α∣ is the number of symbols on the RHS of
the production).
Let state s' be the state now exposed on top of the stack.
Consult GOTO[s', A] . Let this be state j .
Push state j onto the stack.
If ACTION[s, a] == "accept" :
Parsing is complete and successful. Halt.
If ACTION[s, a] == "error" :
Initiate error recovery (or halt and report syntax error).
23. Example of SLR Parsing
(Requires a specific grammar, its LR(0) automaton, FOLLOW sets, and the generated SLR table.
Constructing this fully here is lengthy, but let's assume we have the table for id * id + id $ from section
14).
We'd trace the stack, input, and actions based strictly on the SLR table lookups, rather than the intuitive
guess used in section 14.
Example Trace (using a hypothetical table):
Stack
Input
Action Lookup
Action
Next
State
0
id * id + id
$
ACTION[0,
id]
shift 2
2
02
* id + id $
ACTION[2,
*]
reduce T -> id (Rule 4, assuming
T->id)
0
* id + id $
GOTO[0, T]
push state 3
3
03
* id + id $
ACTION[3,
*]
shift 4
4
034
id + id $
ACTION[4,
id]
shift 2
2
0342
+ id $
ACTION[2,
+]
reduce T -> id (Rule 4)
034
+ id $
GOTO[4, T]
push state 5
0345
+ id $
ACTION[5,
+]
reduce E -> T (Rule 2, assuming
E->T)
034
+ id $
GOTO[4, E]
push state 6
6
0346
+ id $
ACTION[6,
+]
shift 7
7
03467
id $
ACTION[7,
id]
shift 2
2
03467
2
$
ACTION[2,
$]
reduce T -> id (Rule 4)
03467
$
GOTO[7, T]
push state 8
03467
8
$
ACTION[8,
$]
reduce E -> E+T (Rule 1)
034
$
GOTO[4, E]
push state 6
0346
$
ACTION[6,
$]
accept
5
8
6
(Note: This trace is illustrative; the exact states/rules depend heavily on the specific grammar and table).
24. Shift-Reduce/Reduce-Reduce Conflicts and Other Errors in Bottom-up Parsing
During the construction of the parsing table (especially for SLR, LALR, LR(1)), conflicts may arise,
indicating the grammar is not suitable for that specific parsing method or is ambiguous.
Shift-Reduce Conflict: Occurs in a state i if the table construction algorithm finds that for some
terminal a :
There is an item [A -> α . a β] in Ii suggesting a "shift" action ( ACTION[i, a] = shift j ).
AND there is a complete item [B -> γ .] in Ii where a is in FOLLOW(B) , suggesting a "reduce"
action ( ACTION[i, a] = reduce B -> γ ).
The parser doesn't know whether to shift a or reduce by B -> γ .
Example: The dangling else often causes S/R conflicts if not handled by grammar rewriting or
disambiguation rules.
Reduce-Reduce Conflict: Occurs in a state i if the table construction algorithm finds that for some
terminal a :
There is a complete item [A -> α .] in Ii where a is in FOLLOW(A) , suggesting ACTION[i, a] =
reduce A -> α .
AND there is another complete item [B -> β .] in Ii where a is also in FOLLOW(B) , suggesting
ACTION[i, a] = reduce B -> β .
The parser doesn't know which rule to reduce by. This often indicates a more serious grammar
ambiguity.
Other Errors: Blank entries in the ACTION/GOTO table represent situations where the parser
encounters an input symbol for which no valid shift or reduce action is defined in the current state.
When the parser consults the table and finds such an empty/error entry during runtime, it signifies a
syntax error in the input program.
SLR is prone to conflicts because its reduce decisions rely only on FOLLOW sets, which provide less
context than the lookahead symbols used in LALR and LR(1) table construction. If a grammar has conflicts
under SLR, it might be parsable using LALR or LR(1), or it may need to be rewritten.
25. Importance of Symbol Tables
The Symbol Table is a crucial data structure used by virtually all phases of the compiler. Its importance
stems from:
1. Storing Identifier Information: It acts as a database to store essential information about all the
identifiers (variables, constants, functions, procedures, classes, types, labels, etc.) used in the source
program.
2. Scope Management: Helps manage the scope rules of the language (determining where an identifier
is visible and valid).
3. Semantic Checking: Essential for semantic analysis, particularly for type checking (looking up the
types of operands) and ensuring identifiers are declared before use.
4. Code Generation: Provides information needed during code generation, such as the type of a variable
(to determine storage size), its assigned memory location (address or offset), function parameters,
etc.
5. Inter-Phase Communication: Serves as a central repository allowing different compiler phases to
share information about identifiers. The lexical analyzer might enter identifiers, the semantic analyzer
adds type/scope info, and the code generator retrieves addresses.
6. Error Reporting: Helps provide more informative error messages (e.g., "Undeclared identifier 'x'",
"Type mismatch in assignment to 'y'").
Without an effective symbol table, compiling moderately complex programs would be practically
impossible.
26. Scopes in Program and Nesting of Symbol Tables
Scope: The region of the program text within which a particular declaration of an identifier is active or
visible. Different languages have different scoping rules (e.g., lexical/static scope vs. dynamic scope).
Most modern languages use lexical scope, where scope is determined by the structure of the code
text.
Common Scope Types:
Global/File Scope: Visible throughout the entire program file or module.
Function/Procedure Scope: Visible only within a specific function or procedure.
Block Scope: Visible only within a specific block of code (e.g., inside {...} in C++/Java, or
an if / while block).
Class Scope: Visible within a class definition.
Nesting of Scopes: Scopes can be nested inside other scopes (e.g., a block scope inside a function
scope, which is inside a global scope). This creates a hierarchy.
Symbol Table Implementation for Nested Scopes: To handle nested scopes correctly (where an inner
declaration can hide an outer one with the same name), symbol tables are often implemented using:
Stack of Tables: A common approach is to maintain a stack of symbol tables (often implemented
as hash tables or balanced trees).
When entering a new scope (e.g., function entry, block start), a new empty table is pushed
onto the stack. Declarations within that scope are added to this top table.
When searching for an identifier, the stack is searched from top (current scope) to bottom
(outer scopes) until the identifier is found or the bottom is reached (undeclared).
When exiting a scope, the top table is popped from the stack, effectively removing its
declarations.
Single Table with Scope Information: Alternatively, a single large table can be used, where each
entry is tagged with scope information (e.g., scope level, scope ID). Lookups involve finding
entries matching the name and the appropriate scope(s) according to visibility rules.
27. Important Attributes of Variables and Functions (Stored in Symbol Table)
The symbol table stores various attributes associated with identifiers. The exact attributes depend on the
language and the compiler implementation, but common ones include:
For Variables:
1. Identifier Name: The actual name ("x", "count", etc.).
2. Type: The data type (e.g., int , float , char* , boolean , user-defined type name). Crucial for type
checking and determining storage size.
3. Scope: Information about the scope in which it was declared (e.g., scope level, pointer to scope
structure).
4. Memory Location/Offset: The runtime address or, more commonly, an offset relative to an activation
record (for local variables) or a static data area (for global/static variables). Used by the code
generator.
5. Size: The amount of memory required (derived from the type).
6. Storage Class: (e.g., static , extern , auto , register hints).
7. Access Modifiers: (e.g., public , private , protected in object-oriented languages).
8. Other Flags: Is it a constant? Has it been initialized? Is it a parameter?
For Functions/Procedures:
1. Identifier Name: The function name.
2. Return Type: The data type of the value returned by the function (e.g., int , void , float* ).
3. Parameters: Information about the parameters:
Number of parameters.
Type of each parameter (in order).
Passing mechanism (e.g., by value, by reference).
Names of parameters (for internal use or debugging info).
4. Scope: The scope in which the function is declared.
5. Starting Address: The address of the first instruction of the function's code in the target machine
code. Used for generating call instructions.
6. Access Modifiers: (e.g., public , private ).
7. External Linkage: Is the function defined externally?
Storing these attributes allows the compiler to enforce language rules and generate correct, efficient
target code.
0
You can add this document to your study collection(s)
Sign in Available only to authorized usersYou can add this document to your saved list
Sign in Available only to authorized users(For complaints, use another form )