Projeto de Linguagens de Programação
Aula 4: Analisador Sintático 1 Prof Joaquim Pessôa Filho
Sumário
1. Estratégias de parsing
2. Implementação do parsing
3. Gramáticas livre de contexto
4. Gramáticas preditivas
5. Gramáticas ambíguas
Análise Léxica Análise Sintática Análise Semântica
Geração de código intermediário Otimização de código
Geração de código Síntese
Análise
Programa Fonte
Programa Executável
Compilador Atual
Analisador sintático
Analisador sintático ou parser: processo principal do compilador
Coordena as outras etapas
Funções
Verificar a boa formação do programa: quais cadeias pertencem à linguagem
Sintaxe, gramática
Construção da árvore sintática do programa: implícita ou explícita
Tratar erros
Exemplos
while ( <exp> ) <comandos>
id := <exp>
Parsing
Dada uma gramática livre de contexto G:
Parsing possibilita o reconhecimento de uma cadeia e a determinação de sua estrutura de frase
Considerar gramáticas não-ambíguas.
Estratégias de Parsing
O que diferencia as estratégias parsing é a
ordem na qual a árvore sintática da cadeia de entrada é construída
Bottom-up
Top-down
Gramática Micro-English (Exemplo)
Sentença ::= Sujeito Verbo Objeto
.
Sujeito ::= I | a Substantivo | the Substantivo Objeto ::= me | a Substantivo | the Substantivo Substantivo ::= cat | mat | rat
Verbo ::= like | is | see | sees
Sentenças geradas por Micro- English
the cat sees a rat.
I like the mat.
the cat likes me.
I sees the cat.
Micro-English pode gerar sentenças
gramaticalmente incorretas em Inglês Ex: I sees the cat.
sees
Estratégia Bottom-up
rat
a .
cat the
Substantivo
sees
Estratégia Bottom-up
rat
a .
cat the
Substantivo Sujeito
sees
Estratégia Bottom-up
rat
a .
cat the
Substantivo Sujeito
Verbo
sees
Estratégia Bottom-up
rat
a .
cat the
Substantivo Sujeito
Verbo Substantivo
sees
Estratégia Bottom-up
rat
a .
cat the
Substantivo Sujeito
Verbo Substantivo
Objeto
sees
Estratégia Bottom-up
rat
a .
cat the
Substantivo Sujeito
Verbo Substantivo
Objeto Sentença
Estratégia Top-Down
sees a rat .
cat the
Sujeito Verbo Objeto
Sentença
.
Estratégia Top-Down
sees a rat .
cat the
Sujeito Verbo Objeto
Sentença
.
Substantivo
Estratégia Top-Down
sees a rat .
cat the
Sujeito Verbo Objeto
Sentença
.
Substantivo
Estratégia Top-Down
sees a rat .
cat the
Sujeito Verbo Objeto
Sentença
.
Substantivo
Estratégia Top-Down
sees a rat .
cat the
Sujeito Verbo Objeto
Sentença
.
Substantivo Substantivo
Estratégia Top-Down
sees a rat .
cat the
Sujeito Verbo Objeto
Sentença
Substantivo Substantivo
Recursive-descent parsing (Exemplo Micro-English)
private void parseNoun ();
// analisa um substantivo (cat, mat ou rat) private void parseVerb ();
// analisa um verbo (like, sees) private void parseSubject ();
// analisa um sujeito (I ou a rat) private void parseObject ();
// analisa um objeto (me ou a rat) private void parseSentence ();
// analisa a sentença
Recursive-descent parsing
O método parseSentence delega quase todo o trabalho para os outros métodos, e depois que todos os métodos foram executados,
aceita o “.” (ponto do final da sentença)
Implementação do parser
É preciso definir uma classe contendo todos os métodos de parsing
public class Parser{
private TerminalSymbol currentTerminal;
... // Métodos auxiliares ... // Métodos de parsing }
Implementação do parser
private void accept (TerminalSymbol expectedTerminal)
if(currentTerminal é igual ao expectedTerminal)
...
else
reportar o erro ] }
Implementação do parser
Métodos de parsing (exemplo):
private void parseSentence () { Sentence
::=
parseSubject(); Subject
parseVerb(); Verb
parseObject(); Object
accept(‘.’); .
}
Implementação do parser
private void parseSubject () {
if(currentTerminal é igual a ‘I’) Subject ::=
accept(‘I’); I
else |
if(currentTerminal é igual a ‘a’){
accept(‘a’); a
parseNoun (); Noun
}else |
if(currentTerminal é igual a ‘the’){
accept(‘the’); the
parseNoun (); Noun
}else{
Reportar o erro sintático
}
}
Implementação do parser
private void parseNoun () {
if(currentTerminal é igual a ‘cat’) Noun :: =
accept(‘cat’); cat
else |
if(currentTerminal é igual a ‘mat’){
accept(‘mat’); mat
}else |
if(currentTerminal é igual a ‘rat’){
accept(‘rat’); rat
}else{
Reportar o erro sintático
}
}
Implementação do parser
O parser é iniciado pelo método:
public void parse () {
currentTerminal = Primeiro terminal da cadeia;
parseSentence();
}
Observação Importante
O parser visto não constrói explicitamente a árvore sintática, mas determina a estrutura de frase da cadeia de entrada
Gramáticas livre de contexto
Uma sintáxe para programas de uma-linha
S → S; S S → id := E S → print (L) E → id
E → num E → E + E E → (S, E) L → E
L → L, E
Exemplo
Uma sentença na linguagem desta gramática é
id := num; id := id + (id := num + num, id)
onde o texto fonte (antes da análise léxica) pode ter sido
a : = 7; b : = c + (d : = 5 + 6, d)
Derivações
Começando no ponto inicial:
S S ; S
S ; id := E
id := E; id := E id := num ; id := E
id := num ; id := E + E id := num ; id := E + (S, E) id := num ; id := id + (S, E)
id := num ; id := id + (id := E, E)
id := num ; id := id + (id := E + E, E) id := num ; id := id + (id := E + E, id ) id := num ; id := id + (id := num + E, id) id := num ; id := id + (id := num + num, id)
Árvores de Parse
Gramáticas ambíguas
Considerando a gramática:
E → id E → num E → E * E E → E / E E → E + E E → E − E E → (E)
Gramáticas ambíguas
Uma gramática é ambígua se ela pode
derivar uma sentença com duas árvores de parse diferentes
Gramáticas ambíguas
Exemplo: 1-2-3 e 1+2*3
Gramáticas ambíguas
Analisando agora a gramática:
E → E + T E → E − T E → T
T → T * F T → T / F T → F
F → id F → num F → (E)
id * num + num * id / num – id
Marcador de fim de arquivo
Parsers deve não somente ler símbolos terminais, tais como +, −, num, e outros, mas também o
marcador de fim de arquivo ($).
S → E $ E → E + T E → E − T E → T
T → T * F T → T / F T → F F → id F → num F → (E)
Parsing preditivo
Algumas gramáticas são fáceis para analisar gramaticalmente usando um algoritmo simples conhecido como descendente recursivo.
Na essência, cada produção gramatical torna-se uma clausula de uma função recursiva.
S → if E then S else S S → begin S L
S → print E L → end
L → ; S L
E → num = num
Parsing preditivo
Um parser descendente recursivo para essa
linguagem tem uma função para cada não-terminal e uma cláusula para cada produção.
final int IF=1, THEN=2, ELSE=3, BEGIN=4, END=5, PRINT=6, SEMI=7, NUM=8, EQ=9;
int tok = getToken();
void advance() {tok=getToken();}
void eat(int t) {
if (tok==t) advance();
else error();
}
Parsing preditivo
void S() {switch(tok) {
case IF: eat(IF); E(); eat(THEN); S();
eat(ELSE); S(); break;
case BEGIN: eat(BEGIN); S(); L(); break;
case PRINT: eat(PRINT); E(); break;
default: error();
}}
void L() {switch(tok) {
case END: eat(END); break;
case SEMI: eat(SEMI); S(); L(); break;
default: error();
}}
void E() { eat(NUM); eat(EQ); eat(NUM); }
Considerando a gramática
S → E $ E → E + T E → E − T E → T
T → T * F T → T / F T → F
F → id F → num F → (E)
Parsing preditivo
void S() { E(); eat(EOF); } void E() {switch (tok) {
case ?: E(); eat(PLUS); T(); break;
case ?: E(); eat(MINUS); T(); break;
case ?: T(); break;
default: error();
}}
void T() {switch (tok) {
case ?: T(); eat(TIMES); F(); break;
case ?: T(); eat(DIV); F(); break;
case ?: F(); break;
default: error();
}}
Exercícios
1. Traduza cada dessas expressões regulares numa gramática livre de contexto:
1. ( (xy*x) | (yx*y) )?
2. ( (0 | 1)+ "." (0 | 1)* ) | ( (0 | 1)* "." (0 | 1)+ )
Exercícios
2. Escreva uma gramática não-ambígua para cada linguagem seguinte:
1. Palíndromes sobre o alfabeto {a, b}.
2. Strings como a expressão regular a*b* e tem mais a's que b's.
3. Parênteses e colchetes balanceados. Exemplo:
([[](()[()][])])
4. Blocos de comandos em C onde os pontos-e- vírgulas terminam os comandos:
{ expression; { expression; expression; } expression; }