Você está na página 1de 62

Compiladores

Analisador Sintático
Prof. Raimundo BARRETO
DCC/UFAM
Introdução
 Toda linguagem de programação tem regras que
descrevem sua estrutura sintática (ou sintaxe)

 A sintaxe de uma LP pode ser descrita por uma


gramática livre de contexto descrita em notação BNF

 A análise sintática deve reconhecer a estrutura global


do programa, por exemplo, verificando que programas,
comandos, declarações, expressões, etc têm as regras
de composição respeitadas.
Introdução
 O uso de gramáticas traz vantagens na escrita de
compiladores pelas seguintes razões:
 Dá uma especificação sintática precisa de uma LP;
 Pode-se automatizar o processo de construção do analisador
sintático;
O gerador automático pode revelar certas ambigüidades
sintáticas;
 Facilita a compilação e a detecção de erros de programas fonte;
e
 Facilita
incorporação de novas construções sintáticas que
surgem com a evolução de uma LP.
Analisadores Sintáticos
 Dada uma gramática G(S), verificar se uma dada sentença w
pertence ou não à L(G) é obter uma árvore de derivação
sintática (ADS) para w
 O analisador sintático de um compilador nada mais é do que um
construtor de ADS
 Quando ele consegue construir uma ADS para uma sentença w
(um programa), dizemos que w está sintaticamente correta
 Caso contrário, dizemos que w está sintaticamente incorreta
 Uma gramática é ambígua se, para alguma cadeia x, existem duas
ou mais árvores de derivação.
Analisadores Sintáticos
 Osanalisadores sintáticos podem ser
classificados basicamente em dois
grupos: descendentes e ascendentes
Analisador Sintático Descendente
 Tenta construir a ADS para uma sentença w a partir do
símbolo inicial S (raiz), aplicando regras de produção até
produzir todos os símbolos (folhas) de w.
 Em outras palavras, nos métodos descendentes, a árvore
de derivação correspondente a w é construída de cima
para baixo, ou seja, da raiz (o símbolo inicial S) para as
folhas, onde se encontra w.
Analisador Sintático Descendente
Analisador Sintático Ascendente
 Tenta construir a ADS para uma sentença w a partir dos
símbolos de w (folhas), fazendo reduções (substituir o
lado direito de uma regra pelo seu lado esquerdo) até
obter o símbolo inicial S (raiz).
 Em outras palavras, nos métodos ascendentes, a árvore
de derivação correspondente a w é construída de baixo
para cima, ou seja, das folhas, onde se encontra w, para a
raiz, onde se encontra o símbolo inicial S.
Analisador Sintático Ascendente
Analisador sintático descendente
 Descendente com backup
 Descendente recursivo
 Problema com recursividade
 Descendente preditivo ou Analisador de
Gramáticas LL(K)
Analisador sintático descendente
 Descendente com backup
 Descendente recursivo
 Problema com recursividade
 Descendente preditivo ou Analisador de
Gramáticas LL(K)
Descendente com backup
 É o mais antigo método de construção de ADS.
 Usando tentativa-e-erro, tenta derivar uma sentença
esgotando todas as possíveis opções de derivação
 Caso uma escolha tenha sido infeliz, é selecionada outra
derivação e o processo continua.
 O retorno para os pontos com outras possibilidades de
derivação é feito até que se tenha analisado a sentença
inteira, ou até que seja encontrada uma folha que não
seja reconhecida depois de esgotar-se todas as regras de
produção da gramática.
Descendente com backup
Descendente com backup
 O registro de que regra de produção foi adotada em
determinado ponto da análise, que porção da sentença
analisada já foi lida, etc. é mantido em uma estrutura tipo
pilha para facilitar o mecanismo de tentativa-e-erro do
método descendente com backup.
 Por gastar muita memória para registrar os passos que
adota durante a análise de uma sentença, além de ser
demorado, este método praticamente não é mais usado
na construção de compiladores.
Analisador sintático descendente
 Descendente com backup
 Descendente recursivo
 Problema com recursividade
 Descendente preditivo ou Analisador de
Gramáticas LL(K)
Descendente Recursivo
 Para quem dispõe de uma linguagem com recursividade
para a implementação de um compilador, este método é
o mais indicado pois aproveita a estrutura da gramática
de forma completa e não impõe restrições à ela (a única
restrição é a de não haver recursividade a esquerda).
 O analisador sintático recursivo descendente é escrito na
forma de um conjunto de procedimentos, sendo
associado a cada procedimento um elemento não-
terminal da gramática.
Descendente Recursivo
 Por exemplo, dada a gramática:
<expr> ::= <termo> + <expr> | <termo>
<termo> ::= <fator> * <termo> | <fator>
<fator> ::= <primário> ** <fator> | <primário>
<primário> ::= IDENT | NÚMERO | ( <expr> )
 podemos reescrevê-la na forma abaixo
(1) <expr> ::= <termo> { + <expr> }*
(2) <termo> ::= <fator> { * <termo> }*
(3) <fator> ::= <primário> { ** <fator> }*
(4) <primário> ::= IDENT | NÚMERO | ( <expr> )
 onde {w}+ significa uma ou mais ocorrências de w e
{w}* significa zero ou mais ocorrências de w.
Descendente Recursivo
 Podemos, agora, escrever um algoritmo que faça
a análise de sentenças geradas a partir dessa
gramática.
Descendente Recursivo
Procedimento analisador sintático {
obtenha_símbolo(); /* chama o léxico */
EXPR(); }
Procedimento EXPR() {
TERMO();
se símbolo_lido = '+'
então { obtenha_símbolo(); EXPR(); } }
Procedimento TERMO() {
FATOR();
se símbolo_lido = '*'
então { obtenha_símbolo(); TERMO(); } }
Procedimento FATOR() {
PRIMÁRIO();
se símbolo_lido = '**'
então { obtenha_símbolo(); FATOR(); } }
Descendente Recursivo
Procedimento PRIMÁRIO() {
se símbolo_lido = IDENT
então { /* trate adequadamente um identificador */
obtenha_símbolo(); }
senão se símbolo_lido = NÚMERO
então { /* trate adequadamente um número */
obtenha_símbolo(); }
senão se símbolo_lido = '('
então { obtenha_símbolo(); EXPR();
se símbolo ` ')'
então ERRO( "falta )" );
senão obtenha_símbolo(); } }
Analisador sintático descendente
 Descendente com backup
 Descendente recursivo
 Problema com recursividade
 Descendente preditivo ou Analisador de
Gramáticas LL(K)
Problemas de recursividade
 Para que se possa construir um analisador sintático
descendente, uma gramática não pode ter regras
recursivas à esquerda (diretas ou indiretas).
 Uma gramática é recursiva à esquerda se tem produções
da forma:
U ⇒ Uα ou
 U ⇒+ Uα

 Nesse caso, qualquer algoritmo que implemente um


analisador descendente vai entrar em ciclo ("loop")
infinito.
Problemas de recursividade
 Podemos resolver o problema de recursividade direta à
esquerda com uma transformação muito simples na
gramática.
 Regras do tipo:
A ::= Aα | β
 cujo objetivo é produzir cadeias da forma:
 β,
 βα,
 βαα, ...
 devem ser transformadas em:
A ::= βA'
 A'::= α A' | ε
Problemas de recursividade
 Por exemplo, dada a gramática:
 <expr> ::= <expr> + <termo> | <termo>
 <termo> ::= <termo> * <fator> | <fator>
 <fator> ::= IDENT | ( <expr> )
 podemos produzir a versão:
 <expr> ::= <termo> <expr'>
 <expr'> ::= + <termo> <expr'> | e
 <termo> ::= <fator> <termo'>
 <termo'> ::= * <fator> <termo'> | e
 <fator> ::= IDENT | ( <expr> )
 que não mais contém recursividade à esquerda (na verdade a
recursividade foi transferida para a direita).
Analisador sintático descendente
 Descendente com backup
 Descendente recursivo
 Problema com recursividade
 Descendente preditivo ou Analisador de
Gramáticas LL(K)
Descendente preditivo
 O analisador sintático descendente recursivo tem
algumas restrições para sua implementação.
 Uma delas, já abordada, diz respeito ao uso de produções
recursivas à esquerda que levam o analisador à um ciclo
("loop") infinito.
 Outra restrição diz respeito à ocorrências de produções
do tipo:
(1) A ::= αβ
(2) A ::= αγ
 a partir do ponto A na ADS de uma sentença, podemos
derivar pela regra (1) ou (2) para se chegar à mesma
cadeia a.
Descendente preditivo
 Como resolver esse problema?
 Usando desdobramento da regra de produção:
 (3) A ::= αC
 (4) C ::= β | γ

 Problema resolvido? Sim, podemos implementar o


analisador sintático recursivo para quaisquer gramáticas
que obedeçam essas duas restrições.
 E se não temos disponível uma linguagem de
programação recursiva p/ implementar o analisador?
Descendente preditivo
 Nesse caso podemos utilizar um analisador sintático
descendente preditor (ou analisador de gramáticas
LL(K)).
 A idéia do analisador LL(K) ("Left-to-right Left-most-
derivation K") é de que basta olharmos no máximo K
símbolos à frente na sentença, a partir do ponto em que
estamos na ADS, para que possamos decidir que regra de
produção aplicar.
S ::= aS | bS | c S ::= abS | acS | ad
 G(S) é LL(1) G(S) é LL(2)
 w = abc w = abad
 w = bac w = acad
Descendente preditivo
 Em termos de linguagens de programação, quase sempre
é possível obter-se uma gramática LL(1) que permita o
reconhecimento sintático de programas através de um
analisador LL(1) que é bastante simples de implementar.
 o analisador sintático:
 receberá uma seqüência de entrada (a sentença a ser analisada),
 manipulará uma estrutura de dados tipo pilha (onde monta a
ADS),
 consultará uma tabela de análise sintática (tabela de "parsing")
e
 emitirá uma seqüência de saída (regras que estão sendo
aplicadas).
Descendente preditivo
Descendente preditivo
 A seqüência de entrada é formada pela sentença a ser
analisada, seguida por um símbolo delimitador ($).
 A pilha contém uma seqüência de símbolos da
gramática, precedida pelo indicador de base de pilha ($).
 A tabela de análise sintática é uma matriz M[A,a] onde
'A' é um não-terminal e 'a' é um terminal ou dólar ($).
 A seqüência de saída constará das produções aplicadas a
partir do símbolo inicial (S), na geração da sentença.
Descendente preditivo
 Inicialmente a pilha contém o símbolo inicial da
gramática precedido por dólar ($).
 O analisador sintático, a partir de X, símbolo do topo da
pilha, e próximo_símbolo, o atual símbolo da entrada,
determina sua ação que pode ser uma das quatro
possibilidades a seguir:
 1) Se X é um terminal = próximo_símbolo = $, o analisador
encerra sua atividade e comunica fim da análise sintática com
sucesso;
 2) Se X é um terminal = próximo_símbolo ≠ $, o analisador
elimina X do topo da pilha e avança para o próximo símbolo de
entrada;
Descendente preditivo
 3) Se X é um terminal ≠ próximo_símbolo, o
analisador acusa um erro de sintaxe (chama rotina de
tratamento de erros);
 4) Se X é um não-terminal, o analisador consulta
M[X, próximo_símbolo].
 Se a resposta for uma regra de produção X ::= MVU, o
analisador desempilha X do topo da pilha e empilha UVM
(com M no topo da pilha).
 Para a saída é enviada a regra de produção usada. Se M[X,
próximo_símbolo] = ERRO, o analisador acusa um erro de
sintaxe (chama rotina de tratamento de erro).
Descendente preditivo
 Exemplo: Seja a gramática G abaixo com a respectiva
tabela de análise sintática.
 (1) S ::= aAS
 (2) S ::= b
 (3) A ::= a
 (4) A ::= bSA
Descendente preditivo
 Dada a sentença w = abbab$, o analisador sintático
assumiria as seguintes configurações durante a análise:
Descendente preditivo
Descendente preditivo
 A idéia e o algoritmo do preditor é bastante simples.
 Olhando com mais calma, porém, vemos que está
faltando uma coisa fundamental.
 Como obter a tabela (ou matriz) de análise?
 Para chegarmos até ela, precisamos introduzir dois novos
conceitos (ou relações) em gramáticas.
 São os conceitos de FIRST e FOLLOW.
 Vejamos as definições a seguir.
Descendente preditivo
FIRST
 O conjunto FIRST de uma alternativa α, FIRST(α),
contém todos os terminais que α pode iniciar
 Se α pode produzir a cadeia vazia ε, este ε é incluído
em FIRST(α)
 Encontrar FIRST(α) é trivial quando α inicia de um
terminal
 Mas quando α inicia com um não terminal, digamos
N, temos que encontrar FIRST(N)
 Entretanto, FIRST(N) é a união dos conjuntos FIRST
das alternativas.
Descendente preditivo
Exemplo de FIRST sets
 Por exemplo, considere a seguinte gramática:
 E→TE’; E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; F→(E)|id

 First(F) = {(, id}


 First(E’) = {+, ε}
 First(T’)={*, ε}
 First(T) = First(F)
 First(E) = First(T)
Descendente preditivo
Algoritmo FIRST
 Inicialmente First(A) está vazio, para todo não terminal
A.
 As seguintes regras serão aplicadas até que nenhum
terminal ou ε possa ser adicionado ao conjunto First
1. Se X é terminal, First(X)={X}
2. Se X→ε, adicione ε a First(X)
3. Se X→Y1Y2...Yk, qualquer coisa em First(Y1) está em
First(X), exceto se Y1 derivar ε.
a) Ou seja, se Y1 não derivar ε nada mais precisa ser adicionado a
First(X).
b) Se Y1 derivar ε então tentamos adicionar First(Y2) e assim por diante.
Descendente preditivo
FOLLOW
 Uma complicação ocorre com o caso das alternativas
vazias
 Uma vez que não inicia por nenhum token, como saber
se é a alternativa correta?
 Quando um não-terminal N produz uma cadeia não-
vazia, vemos um token que N pode iniciar, FIRST(N)
 Quando N produz a cadeia vazia, vemos um token que
pode seguir N, FOLLOW(N)
 Neste caso, escolhemos alternativa vazia quando
encontramos um token que pode seguir N.
Descendente preditivo
Algoritmo FOLLOW
 Inicialmente, para todos os não-terminais A da
gramática G, todos os conjuntos Follow(A) estão
vazios, excetuando-se Follow(S)= { $ }.
 Regras de inferência
1. Coloque $ em FOLLOW(S), onde S é o símbolo inicial
2. Para cada regra da forma M→αNβ, FOLLOW(N) deve
conter todos os tokens em FIRST(β), excluindo ε, uma vez
que FIRST(β) já o contém
3. Para cada regra da forma M→αN ou M→αNβ onde
FIRST(β) contém ε, FOLLOW(N) deve conter todos os
tokens em FOLLOW(M)
Descendente preditivo
Algoritmo FOLLOW
 Inicia atribuindo o conjunto vazio para todos os
conjuntos FOLLOW de todos os não-terminais
 FOLLOW usa os conjuntos FIRST obtidos
anteriormente
 A segunda regra de inferência diz que se um não-
terminal N é seguido por alguma calda (tail) alternativa
β, N pode ser seguida por qualquer token que β pode
iniciar (FIRST)
 A terceira regra é mais sutil: Se β pode produzir a
cadeia vazia, qualquer token que pode seguir
(FOLLOW) M, pode também seguir N.
Descendente preditivo
Exemplo FOLLOW
 A gramática é:
 E→TE’; E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}

 FIRST(T') = { *, ε}

 Pela regra 1: FOLLOW(E) = {$}


Descendente preditivo
Exemplo FOLLOW
 A gramática é:
 E→TE’; E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; →(E)|id
F→
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}

 FIRST(T') = { *, ε}

 FOLLOW(E) = {$}

 Pela regra 2 “)” é adicionado em FOLLOW(E)


Descendente preditivo
Exemplo FOLLOW
 A gramática é:
→TE’;
 E→ E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}

 FIRST(T') = { *, ε}

 FOLLOW(E) = {), $}

 Pela regra 3 {), $} (que é FOLLOW(E)) é adicionado a


FOLLOW(E’)
Descendente preditivo
Exemplo FOLLOW
 A gramática é:
→TE’;
 E→ E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}
 FIRST(T') = { *, ε}
 FOLLOW(E) = FOLLOW(E') = {), $}
 Pela regra 3 e como E’⇒ ε então FOLLOW(E) é
adicionado em FOLLOW(T)
 Portanto, FOLLOW(T)= {), $}
Descendente preditivo
Exemplo FOLLOW
 A gramática é:
→TE’;
 E→ E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}
 FIRST(T') = { *, ε}
 FOLLOW(E) = FOLLOW(E') = FOLLOW(T)= {), $}
 Pela regra 2, tudo o que esteja em FIRST(E’), exceto ε,
deve ser colocado em FOLLOW(T). Nesse caso,
FOLLOW(T)= {+, ), $}
Descendente preditivo
Exemplo FOLLOW
 A gramática é:
 E→TE’; E’→+TE’|ε; →FT’;
T→
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}
 FIRST(T') = { *, ε}
 FOLLOW(E) = FOLLOW(E') = {), $}
 FOLLOW(T)= {+, ), $}
 Pela regra 3 e como T’⇒ ε então FOLLOW(T) é adicionado em
FOLLOW(F). Nesse caso, FOLLOW(F) = {+, ), $}
 Pela regra 2, tudo o que esteja em FIRST(T’) exceto ε deve ser
colocado em FOLLOW(F). Nesse caso, o “*” tem que ser adicionado.
 Portanto, FOLLOW(F) = {*, +, ), $}
Descendente preditivo
Exemplo FOLLOW
 A gramática é:
 E→TE’; E’→+TE’|ε; →FT’;
T→
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}
 FIRST(T') = { *, ε}
 FOLLOW(E) = FOLLOW(E') = {), $}
 FOLLOW(T)= {+, ), $}
 FOLLOW(F) = {*, +, ), $}
 Pela regra 3 e como T’⇒ ε então FOLLOW(T) é adicionado em
FOLLOW(T’). Nesse caso, FOLLOW(T’) = {+, ), $}
 Portanto, o resultado fica...
Descendente preditivo
Exemplo FOLLOW
 A gramática é:
 E→TE’; E’→+TE’|ε; T→FT’;
 T’→*FT’|ε; F→(E)|id
 Temos:
 FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
 FIRST(E') = { +, ε}

 FIRST(T') = { *, ε}

 FOLLOW(E) = FOLLOW(E') = { ), $}

 FOLLOW(T) = FOLLOW(T') = { +, ), $}

 FOLLOW(F) = { +, *, ), $}
Descendente preditivo
Tabela de parsing
Descendente preditivo
Construção da Tabela de parsing
Algoritmo: Construção da tabela de parsing
Início
→α da gramática faça:
Para cada produção A→α
para cada símbolo terminal a em FIRST(αα) faça:
→α em M[A, a]
adicione a produção A→α
se ε está em FIRST(α
α)
→α em M[A, b], para
adicione a produção A→α
cada terminal b em FOLLOW(A)
se ε está em FIRST(α
α) e $ está em FOLLOW(A)
→α em M[A, $]
adicione a produção A→α
Indique erro em cada entrada indefinida de M.
Fim
Descendente preditivo
Construção da Tabela de parsing
(1)E → TE' (5) T' → *FT'
(2)E' → +TE' (6) T' → ε
(3)E' → ε (7) F → ( E )
(4)T → FT' (8) F → id
Descendente preditivo
Construção da Tabela de parsing
(1)E → TE' (5) T' → *FT'
(2)E' → +TE' (6) T' → ε
(3)E' → ε (7) F → ( E )
(4)T → FT' (8) F → id

Regra 1
FIRST(TE’) =
FIRST{T} = {id, (}
Descendente preditivo
Construção da Tabela de parsing
(1)E → TE' (5) T' → *FT'
(2)E' → +TE' (6) T' → ε
(3)E' → ε (7) F → ( E )
(4)T → FT' (8) F → id

Regra 1
Uma vez que
FIRST(+)={+}
Descendente preditivo
Construção da Tabela de parsing
(1)E → TE' (5) T' → *FT'
(2)E' → +TE' (6) T' → ε
(3)E' → ε (7) F → ( E )
(4)T → FT' (8) F → id

Regra 2, uma
vez que
FOLLOW(E’)
={), $}
Descendente preditivo
 O algoritmo dado é válido para qualquer
gramática
 Porém, para algumas gramáticas, a matriz M
pode possuir algumas entradas multiplamente
definidas
 por exemplo, se a gramática é recursiva à
esquerda ou ambígua, temos pelo menos uma
entrada multiplamente definida.
Descendente preditivo
 Exemplo: A gramática abaixo é ambígua para a sentença
w, e pode ser interpretada de duas formas diferentes.
 <cmd> ::= if <cond> then <cmd> <pelse>
 <cmd> ::= a
 <pelse> ::= else <cmd>
 <pelse> ::= ε
 <cond> ::= b

 W = if <cond> then if <cond> then a else a


Descendente preditivo
W = if <cond> then if <cond> then a else a

if <cond> then
if <cond> then a
else a
ou
if <cond> then
if <cond> then a
else a
Descendente preditivo
 Para essa gramática, teríamos a seguinte matriz
de análise sintática:
Descendente preditivo
 Gramáticas cujas tabelas de análise sintática não
possuem entradas múltiplas definidas são ditas
LL(1).

Você também pode gostar