term() - Al Akhawayn University

advertisement
CSC 3315
Lexical and Syntax Analysis
Hamid Harroud
School of Science and Engineering, Akhawayn University
http://www.aui.ma/~H.Harroud/csc3315/
CSC3315 (Spring 2009)
1
Syntax Description vs. Syntax Analysis

Syntax Description: the set of rules that can
be used to generate any sentence in the
language


We can construct derivations (or parse trees) to
generate (synthesize) arbitrary sentences
Syntax Analysis: the reverse process

Given a sentence, obtain the derivation (or parse
tree) that would generate this sentence based on
some grammar
The Parsing Problem

Given the sequence of tokens of a source
program (a sentence), we ask: is it
syntactically valid, i.e. could it be generated
from that grammar?

If it is not; find all syntax errors (or as many as
possible)
» for each error, produce a diagnostic message and
recover quickly– i.e. do not “crash”

If it is; construct the corresponding parse tree
(i.e. the derivation)
The Parsing Problem

A parser is a program that solves the parsing
problem



It is the second phase of a compiler
Parse tree constructed by parser is used in the
subsequent compilation phase: semantic analysis and
code generation
A recognizer is similar to a parser, except that it
does not produce the parse tree- it only gives a
Yes or No answer
Syntax Description vs. Syntax Analysis


Parser design is nearly always directly based on
a grammar description (BNF) of language
syntax
Two categories of parsers


Top down - parse tree is constructed starting at root,
using an order of a leftmost derivation
Bottom up - parse tree is constructed starting at
leaves, using an order of the reverse of a rightmost
derivation
Top-down Parsing: LL Parsing

Top-down parsers are also called LL-parsers



First L: Scan input sentence from left to right
Second L: Produce a left-most derivation
Scans the token stream from left to right:
For each input token, it decides which rule to use to expand the
leftmost non-terminal in the current sentential form
 This decision is based only on whether the leftmost terminal
generated by the current leftmost non-terminal matches the current
input token
=> one token of lookahead
=> LL parsers that use one token of lookahead are called LL(1) parsers

LL(1) Parsing


The LL Parser must expand <A> using some rule of the
form: <A>  RHS
Obviously, this decision must be based both on the
grammar and the unparsed portion of the token stream

A LL(1) parser determines the correct rule to use to expand a nonterminal <A> based only on the first token generated by <A>

Definition: if X is the RHS of some grammar rule then FIRST(X) =
{a | =>* aY}
 where a : a terminal symbol
 =>* : zero or more derivation steps
LL(1) Parsing

Example:

Grammar: <A>  b<B> | c<B>b | a
Suppose the input token currently being processed is c
 If the current sentential form is x<A>a, then there are
three possible rules we can apply, and the possible
next sentential forms are then: xb<B>a, xc<B>ba, and
xaa
=> But which rule to use next?
=> Choose the rule such that the leftmost terminal
obtained when <A> is expanded matches our current
input token “c”

LL(1) Parsing

Example (cont.)
Consider now a slightly modified version of
this grammar:<A>  c<B> | c<B>b | a
=> this grammar actually cannot be used with
a LL(1) parser, since it is sometimes
impossible to decide on next rule based on
the current token only

Recursive-Descent Parsing


This is a recursive implementation of LL(1) parsers
Based directly on BNF description of the language:



There is a subprogram for each non-terminal in the
grammar, which can parse sentences generated by that
non-terminal
EBNF is ideally suited for being the basis for a recursive
descent parser, because EBNF tends to minimize the
number of non-terminals in grammar
In a way, this is analogous to how the lexical
analyzer is implemented directly from the state
diagram of the corresponding FA
Recursive-Descent Parsing


Suppose a non-terminal <A> has only one RHS
The subprogram for <A> is implemented as
follows:



Scan the symbols of RHS one by one from left to right
For each terminal symbol in the RHS, compare it with the
next input token; if they match, then read the next input
token and continue; else report an error
For each non-terminal symbol in the RHS, call the
subprogram associated with that non-terminal
Recursive-Descent Parsing


Example: <A>  a<B>cb
The subprogram for non-terminal <A>
does the following:




Check if next token == ‘a’ else error()
Call subprogram for non-terminal <B>
Check if next token == ‘c’ else error()
Check if next token == ‘b’ else error()
Recursive-Descent Parsing


Suppose a non-terminal <A> has more than one
RHS
The subprogram for <A> is implemented as
follows:


First find rule <A>  X such that FIRST(X) == next
token
» If none of the RHS’s of <A> satisfies this, then
report an error
» If more than one RHS of <A> satisfies this, then the
grammar is not appropriate for recursive descent
parsing to begin with!
Continue as in the previous case
Recursive-Descent Parsing

Assumptions:



We have a lexical analyzer subprogram Lex(),
which whenever called, puts the next token code
in a global variable nextToken
At the beginning of every parsing subprogram,
the next input token is in nextToken
Obviously, the first subprogram called is the
one associated with the start symbol of the
grammar
Recursive-Descent Parsing

Example: consider the following grammar
<A>  a<B>cb | c<B> | ba<C>



Let ASub be subprogram associated with
nonterminal <A>
Let BSub be subprogram associated with
nonterminal <B>
Let CSub be subprogram associated with
nonterminal <C>
Recursive-Descent Parsing
Parser() { //this is the main function of the parser of this grammar
Lex(); //get the first token in the source program
ASub(); //call the subprogram for the start symbol of the grammar
}
ASub() {
switch (nextToken)
case ‘a’:
Lex();
BSub();
if nextToken != ‘c’ then error();
Lex();
if nextToken != ‘b’ then error();
Lex();
break;
case ‘c’:
Lex();
BSub();
break;
default:
CSub();
}
Recursive-Descent Parsing
An example in EBNF:
<expr>  <term> {(+ | -) <term>}
<term>  <factor> {(* | /) <factor>}
<factor>  id | ( <expr> )



Let expr() be subprogram associated with non-terminal
<expr>
Let term() be subprogram associated with non-terminal
<term>
Let factor() be subprogram associated with nonterminal <factor>
Recursive-Descent Parsing
/* Parse strings in the language
generated by the rule:
<expr>  <term> {(+ | -) <term>}
*/
void expr() {
term(); /* Parse the first term */
while (nextToken == PLUS_CODE || nextToken == MINUS_CODE)
{
lex();
term();
}
}
Recursive-Descent Parsing
/* Parse strings in the language
generated by the rule:
<term>  <factor> {(* | /) <factor>}
*/
void term() {
factor(); /* Parse first factor */
while (nextToken == MULT_CODE || nextToken == DIV_CODE)
{
lex();
factor();
}
}
Recursive-Descent Parsing
/* Parse strings in the language
generated by the rule:
<factor>  id | (<expr>) */
void factor() {
if (nextToken) == ID_CODE)
lex();
else if (nextToken == LEFT_PAREN_CODE) {
lex();
expr();
if (nextToken == RIGHT_PAREN_CODE)
lex();
else error();
} /* End of else if (nextToken == ... */
else error(); /* Neither RHS matches */
}
Recursive-Descent Parsing
Limitations:

Recursive descent cannot be used with a grammar that:
contains a left recursive rule => an infinite loop!
OR
 is such that we cannot always choose correct RHS based on a single
token of lookahead


These two features are in fact problematic for LL(1) parsers
in general
Left Recursion Problem
Examples:

Direct recursion: <A>  <A> + <B>

Indirect recursion:
<A>  <C>a
<C>  <A>b
=> In both cases, the subprogram for parsing <A> will call
itself indefinitely (hence an infinite loop)

what about non-left recursion; does it always lead to an
infinite loop?
Left Recursion Problem

A grammar can always be converted into one without left
recursion
 EBNF is especially useful for this purpose

Example:
<expr>  <term> | <expr> + <term>
<expr>  <term> {+ <term>}
Pairwise Disjointness


Lack of pairwise disjointness: the inability to determine the
correct RHS (to expand a non-terminal) on the basis of a
single token of lookahead
To make sure a grammar does not have this problem,
grammar must pass the pairwise disjointness test:



For each non-terminal A in the grammar,
Let α1, α2, …, αm be the RHS’s for the m rules in which A appears
on the LHS
For any pair (αi, αj) we must have
FIRST(αi) η FIRST(αj) = Ø
Pairwise Disjointness


The pair disjointness problem can sometimes be solved via
a technique called left factoring
Example:
<variable>  identifier | identifier [<expression>]

<variable>  identifier <new>
<new>  [<expression>] | ε
<variable>  identifier [ [<expression>] ]
Pairwise Disjointness

Exercise:
<S>  <S>a | b

What is the language generated by this grammar?
For which input token streams will the subprogram
for <S> run into an infinite loop?
Does this grammar pass the pairwise disjointness
test?


Pairwise Disjointness

Exercise:
<A>  a | <B>b | <C><B>
<B>  c<C> | c
<C>  b | a<B>

Does this grammar pass the pairwise disjointness test?
If not, re-write grammar to fix the problem.

LR Parsing




LL(k) parsers predict which production to use,
considering only the first k tokens.
What if we could postpone choosing a production
until we see all input tokens corresponding to the
entire right-hand side of a production?
LR(k): rightmost derivation, lookahead of k tokens
How is rightmost derivation compatible with leftto-right parsing of input?
LR Parsing
Top-down parsing



Start at most abstract level (grammar sentence) and work
down to most concrete level (tokens)
Leftmost derivation
LL(k), recursive-descent or predictive parsing
Bottom-up parsing



Work from tokens up to sentences
Rightmost derivation
LR(k)
LR Parsing
LR Parsing




An LR(k) parser uses the contents of its stack and the next
k tokens of the input to decide which action to take. (In
practice, k = 1.)
The parser knows when to shift and reduce by applying a
DFA to the stack.
Edges are labeled with terminals and non-terminals.
Transitions indicate actions such as “shift and go to state
n”, “reduce by rule k”, “accept”.
LR Parsing
Download