A apresentação está carregando. Por favor, espere

A apresentação está carregando. Por favor, espere

Análise Sintática de Descida Recursiva

Apresentações semelhantes


Apresentação em tema: "Análise Sintática de Descida Recursiva"— Transcrição da apresentação:

1 Análise Sintática de Descida Recursiva
Prof. Alexandre Monteiro Baseado em material cedido pelo Prof. Euclides Arcoverde Recife

2 Contatos Prof. Guilherme Alexandre Monteiro Reinaldo
Apelido: Alexandre Cordel /gtalk: Site: Celular: (81)

3 Agenda Introdução Produção Única Múltiplas Produções
Conjuntos FIRST Fatoração à Esquerda Recursão à Esquerda Recursão à Direita Produção Vazia Conjunto FOLLOW Produção de Substituição

4 Análise de Descida Recursiva
É uma análise sintática top-down ou descendente É um parser LL(1) Left-to-right – a ordem de leitura dos tokens é da esquerda para a direita Leftmost derivation – segue a ordem típica de uma derivação mais à esquerda Só olha 1 token por vez

5 Análise de Descida Recursiva
É útil para construir parsers manualmente, por ser fácil de implementar Princípio Básico Se eu quero produzir um não-terminal e sei qual o terminal corrente eu devo saber que regra deve ser aplicada. Porém, requer uma gramática muito bem ajustada Veremos os ajustes necessários Mostraremos como usar a técnica, assumindo que o parser final será uma classe Java

6 Exemplo de Reconhecedor
Gramática: Terminais = {a,b,c} Não-terminais = {S, X} Produções = { S  a X b S  b X a S  c X  b X X  a } Não-terminal inicial = S a b c S a X b b X a X b X Error

7 Classe Parser Atributos típicos Operação principal Métodos auxiliares
A classe Lexer (será o atributo “lexer”) O último token lido (será o atributo “token”) Operação principal Método “parse()” Métodos auxiliares Para aceitar os tokens, se estiverem corretos Para reconhecimento (parsing) de não-terminais

8 Métodos para Tokens Serão dois métodos para aceitar tokens, ambos serão chamados acceptToken() Uma versão receberá o tipo do token que o analisador espera Se o token não for do tipo esperado, reporta um erro A outra versão não receberá parâmetro Usado quando o tipo dele já é conhecido e é válido

9 Métodos para Tokens Ambos os métodos acceptToken atualizam o atributo token, lendo o próximo token A primeira versão (tipo como parâmetro) só lê se o tipo do token anterior estiver correto Assim, só esses dois métodos atualizam o atributo token Portanto, só eles podem chamar o lexer

10 Métodos para Não-Terminais
Cada não-terminal N terá um método específico que chamaremos parseN() O objetivo desse método é reconhecer uma sequência de símbolos que case com alguma das produções de N É aqui que está a principal característica da análise de descida recursiva...

11 Análise de Descida Recursiva
Portanto, cada método parseN() deverá: Identificar a produção adequada N  X1 X2 ...XK Em seguida, deverá chamar o método para reconhecer cada símbolo Xi Se for um não-terminal, chama parseXi() Se for um terminal, chama acceptToken()

12 Produção Única Um não-terminal com apenas uma produção é o caso mais simples de ser tratado Ficará assim frase ::= sujeito predicado “.” void parseFrase() { parseSujeito(); parsePredicado(); acceptToken(PONTO); }

13 Múltiplas Produções Porém, quando houver mais de uma produção, é preciso usar um critério de escolha Por exemplo: Como escolher a produção certa (ou seja, a que representa o código fonte) durante a análise? sujeito ::= “Eu” | “Um” substantivo | “O” substantivo

14 Múltiplas Produções No exemplo anterior, basta olhar o token atual
void parseSujeito() { TokenType tp = this.token.getType(); if (tp == TokenType.EU) { acceptToken(); } else if (tp == TokenType.UM) { parseSubstantivo(); } else if (tp == TokenType.O) { } else { <.. reportar erro ..> }

15 Múltiplas Produções Como fazer no caso geral para escolher a produção?
Exemplo: reconhecer comando comando ::= atribuição | declaração atribuição ::= IDENTIFICADOR = <expressao> ; declaração ::= tipo IDENTIFICADOR ; tipo ::= “int” | “char” | “float”

16 Conjunto FIRST Aplicado a cadeias α quaisquer
É o conjunto de terminais que podem iniciar a cadeia α dada Se a cadeia derivar vazio, também inclui ε Como calcular?

17 Conjunto FIRST FIRST(ε) = { ε } FIRST(a) = { a }
Se α for a cadeia vazia (α = ε) FIRST(ε) = { ε } Se for um terminal a qualquer FIRST(a) = { a } Se for um não-terminal N com as produções N → α | β FIRST(N) = FIRST(α)  FIRST(β)

18 Conjunto FIRST Se for uma cadeia de símbolos α = X1X2...Xk
Depende de X1 Se X1 não pode gerar vazio FIRST (X1X2...Xk) = FIRST(X1) Se X1 pode gerar vazio FIRST (X1X2...Xk) = FIRST(X1)  FIRST(X2...Xk) Calcula para X1 e continua o cálculo recursivamente para o restante da cadeia

19 Múltiplas Produções comando ::= atribuição | declaração
Calculando o FIRST para as duas produções de comando ... comando ::= atribuição | declaração FIRST(atribuição) = FIRST(IDENTIFICADOR = <expressao> ;) = { IDENTIFICADOR } FIRST(declaração) = FIRST(tipo IDENTIFICADOR ;) = FIRST(tipo) = { int , char, float }

20 Múltiplas Produções A solução é comparar o token atual com o FIRST de cada produção de comando... void parseComando() { TokenType token = this.token.getType(); if (token == IDENTIFICADOR) { parseAtribuicao(); } else if (token == INT || token == CHAR || token == FLOAT ) { parseDeclaracao(); } else { throw new CompilerException(...); }

21 Múltiplas Produções No caso geral, para fazer o parsing de um não-terminal N, com produções N  α | β void parseN() { if (token estiver em FIRST(α)) { faz o parsing de α ... } else if (token estiver em FIRST(β)) { faz o parsing de β ... } else { reportar/tratar erro }

22 Análise de Descida Recursiva
Uma limitação dessa técnica é que as produções de um mesmo não-terminal têm que ter seus conjuntos FIRST disjuntos entre si Ou seja, olhando apenas um terminal adiante tem que ser possível escolher a produção da gramática Uma gramática com essa propriedade é chamada uma gramática LL(1) Assim como o parser é dito um parser LL(1)

23 Fatoração à Esquerda Alguns casos em que as produções têm seus conjuntos FIRST idênticos podem ser tratados Um deles é quando as produções têm prefixos comuns Exemplo cmd ::= if exp then cmd else cmd | if exp then cmd | outro

24 Fatoração à Esquerda A solução é parecida com a fatoração em expressões matemáticas Colocar a parte comum em evidência A parte diferente pode se tornar outro não-terminal Exemplo anterior cmd ::= if exp then cmd restoCmd | outro restoCmd ::= else cmd | ε

25 Fatoração à Esquerda Caso geral
Seja N com produções com prefixo comum α Colocando α em comum e criando um novo não-terminal X N → α β1 | α β2 | Ф N → α X | Ф X → β1 | β2

26 Eliminação de Fatoração
Indireta X  a b X  Y c Y  a d Elimina-se a indireção: X  a d c Sem fatoração: X  a Z Z  b Z  d c Direta: X  a b c X  a d e Sem fatoração: X  a Y Y  b c Y  d e

27 Exercícios Resolver fatorações a esquerda: Direta: Indireta: X  a c X
X  a d Indireta: X  a X X  Y c X  d Y  a b

28 Recursão à Esquerda Outra limitação (menos grave) da técnica, é se houver uma produção recursiva à esquerda O que acontece se tentarmos criar o parser diretamente desta gramática? exp ::= exp “+” NUM | NUM

29 Recursão à Esquerda void parseExp() { if (token.getType() == NUM) { parseExp(); acceptToken(MAIS); acceptToken(NUM); } else if (token.getType() == NUM) { acceptToken(); } recursão infiinita ! Qual o problema com esse código? Além dos conjuntos FIRST não serem disjuntos, ele apresenta recursão infinita!

30 Recursão à Esquerda Neste caso, é preciso alterar a gramática para remover a recursão à esquerda É necessário haver outra produção sem a recursão Como reescrever o exemplo anterior para remover a recursão à esquerda? exp ::= exp “+” NUM | NUM

31 Recursão à Esquerda O exemplo anterior reescrito exp ::= NUM restoExpr
restoExpr ::= “+” NUM restoExpr | ε

32 Recursão à Esquerda Caso geral Seja a gramática Comentários N → N α1
A recursão à esquerda serve apenas para produzir repetições de α1 ou α2 (à direita das recursões) A recursão só pára quando o N à esquerda for β1 ou β2 (produções sem recursão à esquerda) N → N α1 | N α2 | β1 | β2 Sejam α1, α2, β1 e β2 cadeias quaisquer

33 Recursão à Esquerda Caso geral (solução)
Criar um novo não-terminal X para criar zero ou mais repetições de α1 ou α2 com recursão à direita Fazer as produções sem recursão de N terminarem com X N → N α1 | N α2 | β1 | β2 N → β1 X | β2 X X → α1 X | α2 X | ε

34 Eliminação da Recursividade Esquerda
Direta: X  X a X  b Sem recursão: X’  ε X’  a X’ X  b X’ Indireta: X  Y a X  b Y  X c Y  d Remove-se a indireção: X  X c a X  d a Resolve-se como no caso anterior

35 Exercícios Resolver recursividades a esquerda: Direta: Indireta:
X  X a X  b Indireta: X  Y a Y  X c Y  d

36 Recursão à Direita para Listas
As recursões servem para permitir repetições de algum tipo de cadeia da gramática Listas/Seqüências de comandos Listas/Seqüências de declarações A recursão à esquerda não pode ser usada, mas vamos ver agora uma maneira de usar a recursão à direita

37 Recursão à Direita para Listas
Exemplo Problemas com “listaComandos” Conjuntos FIRST das produções idênticos: { ID } Decidir quando parar de escolher a recursão listaComandos ::= comando listaComandos | comando comando ::= ID "=" expressao

38 Recursão à Direita para Listas
Uma solução seria usar a fatoração à esquerda, explicada antes Mas tem outra solução mais simples listaComandos ::= comando restoListaCmds restoListaCmds ::= listaComandos |  comando ::= ID "=" expressao

39 Recursão à Direita para Listas
A recursão anterior pode ser reescrita assim: Foi feita uma fatoração à esquerda sem criação de um novo não-terminal Isso foi possível com o operador regular * Zero ou mais repetições listaComandos ::= comando {comando}* comando ::= ID "=" expressao

40 Recursão à Direita para Listas
A parte do operador * pode ser resolvida fazendo um loop Enquanto o token atual for elemento do FIRST de comando, faz o parsing de mais um comando void parseListaComandos() { parseComando(); while (token.getType() == ID) { }

41 Produção Vazia Um tipo de produção que ainda não foi abordada é a produção vazia Um exemplo apareceu quando falamos de fatoração à esquerda restoCmd cmd ::= if exp then cmd restoCmd | outro restoCmd ::= else cmd | ε

42 Produção Vazia Uma solução simples é criar um else com corpo vazio (não lançar erro), após verificar que o token não casa com as outras produções void parseRestoCmd() { if (token.getType() == ELSE) { ... } else { /* do nothing! */ }

43 Produção Vazia Um pequeno problema da abordagem anterior é que erros sintáticos no código fonte só serão identificados depois No método que for chamado após parseRestoCmd Uma abordagem mais robusta para tratar a produção vazia consiste em testar se o token é um dos tipos esperados depois do não-terminal

44 Conjunto FOLLOW FOLLOW (N)
Aplicado a não-terminais N quaisquer É o conjunto de terminais que podem aparecer à direita do não-terminal N, em alguma cadeia derivada pela gramática Se o símbolo inicial deriva uma cadeia “... N x ...” então x faz parte de FOLLOW(A) O símbolo inicial tem $ no conjunto FOLLOW para indicar fim da entrada (EOF) Como calcular?

45 Conjunto FOLLOW Se N for o símbolo inicial
Colocar $ (EOF) no conjunto FOLLOW(N) Para toda produção X → α N β , em que beta não é vazio e não deriva vazio Adicionar tudo de FIRST(β) no FOLLOW(N) Para toda produção X → α N β , em que beta ou é vazio ou deriva vazio Adicionar todo o FOLLOW(X) no FOLLOW(N)

46 Produção Vazia Assim, a maneira mais correta de tratar a produção vazia de um não-terminal N seria testar, no último caso, se o token está no conjunto FOLLOW(N) Se não estiver, o caso else do parseN() já pode reportar um erro imediatamente Alterando o caso geral de “múltiplas produções” mostrado antes...

47 Produção Vazia Seja N um símbolo não-terminal N, com produções N ::= α | β | ε void parseN() { if (token estiver em FIRST(α)) { faz o parsing de α ... } else if (token estiver em FIRST(β)) { faz o parsing de β ... } else if (token estiver em FOLLOW(N)) /* faz nada, produção vazia */ } else { reportar/tratar erro... }

48 Produção de Substituição
Uma produção de substituição de não-terminal Este tipo de produção cria apenas uma chamada de método a mais comando ::= cmdDecisão cmdDecisão ::= if expressao... | switch expressao ... void parseComando() { parseCmdDecisao(); }

49 Produção de Substituição
Para melhorar a eficiência do parser, é melhor trocá-la pelas produções do não-terminal referenciado O exemplo anterior ficaria simplesmente comando ::= if expressao... | switch expressao ...

50 Exemplo Linguagem XPR-0* Descrição Definir
É uma linguagem simples para reconhecer e calcular expressões matemáticas de adição e multiplicação com operandos de apenas um dígito. Os programas desta versão são formados por um único comando, onde um comando pode ser qualquer expressão terminada com ponto-e-vírgula. A semântica (significado) deste comando é a avaliação da expressão seguida de sua impressão na saída padrão Definir Sintaxe abstrata Sintaxe concreta Implementacao * Continuação do código disponibilizado para o lexer “manual”

51 Exemplo Sintaxe Abstrata <program> ::= <command>
<command> ::= <expr> ";" <expr> ::= <expr> "+" <expr> | <expr> "*" <expr> | NUMERO | "(" <expr> ")"

52 Exemplo Sintaxe Concreta <program> ::= <command>
<command> ::= <expr> ";" <expr> ::= <term> <restExpr> <restExpr> ::= "+" <term> <restExpr> | ε <term> ::= <factor> <restTerm> <restTerm> ::= "*" <factor> <restTerm> <factor> ::= NUMERO | "(" <expr> ")"

53 Exemplo Implementar duas versões Classe ParserTemp Classe Parser
Implementação “vazia” da análise sintática Apenas indica se a expressão está ou não correta Classe Parser Construído a partir do ParserTemp Calcula o resultado da expressão Funciona como um interpretador

54 Algoritmos FIRST e FOLLOW Tabela LL(1)
Euclides Arcoverde

55 Algoritmo 1: Cálculo de FIRST
Calcular FIRST(A) para todos os não terminais de uma gramática G Inicialmente para todos os não terminais A da gramática, todos os conjuntos FIRST(A) estão vazios Para cada regra A  B1 ... Bma tal que para todo i = 1, ..., m, Bi  ε, acrescente a a FIRST(A) Para cada regra A  B1 ... BmB tal que, para todo i = 1, ..., m, Bi  ε, acrescente FIRST(B) a FIRST(A) Repita o passo 3 enquanto houver alteração no valor de algum dos conjuntos FIRST

56 Resumo FIRST FIRST() = {} FIRST(t) = {t}
FIRST(XY) = FIRST(X)  FIRST(Y), se X gera  FIRST(XY) = FIRST(X), se X não gera  FIRST(X|Y) = FIRST(X)  FIRST(Y) FIRST(X*) = FIRST(X) 56

57 Algoritmo 1: Exemplo Considere a seguinte gramática:
E -> E + T E -> T T -> T * F T -> F F -> ( E ) F -> a Passo 0: FIRST(E) = FIRST(T) = FIRST(F) =  Passo 1: (Regra 5) FIRST(F) = { ( } (Regra 6) FIRST(F) = { a } Passo 2: (Regra 4) FIRST(T) = FIRST(F) = { (, a } (Regra 2) FIRST(E) = FIRST(T) = FIRST(F) = { (, a }

58 Algoritmo 1: Exercício P -> A B C D A -> ε A -> a A B -> ε
Considere a seguinte gramática: P -> A B C D A -> ε A -> a A B -> ε B -> B b C -> c C -> A B D -> d

59 Algoritmo 2: Cálculo de FOLLOW
Calcular FOLLOW(A) para todos os não terminais de uma gramática G Inicialmente para todos os não terminais A da gramática, todos os conjuntos FOLLOW(A) estão vazios, exceto FOLLOW(S) = { $ } Se há uma regra A  Ba e  = B1, ..., Bm  ε, acrescente “a” a FOLLOW(B) Se há uma regra A  BC e  = B1, ..., Bm  ε, acrescente FIRST(C) a FOLLOW(B) Se há uma regra A  B e  = B1, ..., Bm  ε, acrescente FOLLOW(A) a FOLLOW(B) Repita o passo 4 enquanto houver alteração no valor de algum dos conjuntos

60 Algoritmo 2: Exemplo Considere a seguinte gramática:
E -> E + T E -> T T -> T * F T -> F F -> ( E ) F -> a FIRST(E) = FIRST(T) = FIRST(F) = { (, a } Passo 0: FOLLOW(E) = { $ } FOLLOW(T) = FOLLOW(F) =  Passo 1: (Regra 1) FOLLOW(E) = { $, + } (Regra 3) FOLLOW(T) = { * } (Regra 5) FOLLOW(E) = { $, +, ) } FOLLOW(F) =  Passo 2: (Regra 2) FOLLOW(T) = FOLLOW(E) = { $, +, ), * } (Regra 4) FOLLOW(T) = FOLLOW(F) = { $, +, ), * } Resultado final FOLLOW(E) = { $, +, ) } FOLLOW(T) = FOLLOW(F) = { $, +, ), * }

61 Algoritmo 2: Exercício E -> T E’ T -> F T’ F -> ( E )
Considere a seguinte gramática: E -> T E’ T -> F T’ F -> ( E ) F -> a E’ -> + T E’ E’ -> ε T’ -> * F T’ T’ -> ε FIRST(T) = FIRST(F) = { (, a } FIRST(E’) = { + } FIRST(T’) = { * }

62 Construção da Tabela LL(1)
Para cada produção A ->  Para cada terminal  em FIRST(A), inclua A ->  em M[A, ] Se ε pertence a FOLLOW(), inclua A ->  em M[A, b] para cada terminal b em FOLLOW() Se ε pertence a FIRST() e $ pertence a FOLLOW(A), acrescente também A ->  em M[A, $]

63 Construção da Tabela LL(1)
Para toda regra: X -> A B C: Se A for um terminal: Adicione “ABC” na posição (A,X) da tabela Se A for um não-terminal: Adicione “ABC” na linha X somente nas colunas definidas pelos FIRST(A) Para toda regra: X -> Adicione “” na linha X e nas colunas vazias (error)

64 Exemplo de Tabela LL(1) X -> a X X -> b Produções: S -> a X b
S -> b X a S -> c X -> b X X -> a a b X a X a b c S a X b b X a X b X error

65 Exercícios Produções 1 Produções 2 Produções 3 E -> E + T E -> T
T -> T * F T -> F F -> ( E ) F -> a Produções 2 X > ab X > Yb X > b Y > ca Y > dZ Z > ab Produções 3 E -> T E’ T -> F T’ F -> ( E ) F -> a E’ -> + T E’ E’ -> ε T’ -> * F T’ T’ -> ε

66 Bibliografia AHO, A., LAM, M. S., SETHI, R., ULLMAN, J. D., Compiladores: princípios, técnicas e ferramentas. Ed. Addison Wesley. 2a Edição, 2008 (Capítulo 4)


Carregar ppt "Análise Sintática de Descida Recursiva"

Apresentações semelhantes


Anúncios Google