Análise Sintática de Descida Recursiva

Slides:



Advertisements
Apresentações semelhantes
Software Básico Silvio Fernandes
Advertisements

Tópicos em Compiladores
Como se familiarizar com seu banco de dados usando o SPSS
5.5 – Análise Bottom-Up Tentativa de construir uma árvore sintática para a sentença analisada, começando das folhas, indo em direção à raiz (pós-ordem.
Estruturas de Repetição
Compiladores Claudio Benossi
III – Análise sintáctica
III – Análise sintáctica
Deyvisson, Rafael M.P., Renato, Robson C.F., Rodolfo
Software Básico Silvio Fernandes
Análise Sintática - Continuação
Análise Sintática - Continuação
Aplicações de Pilhas Pilhas são fundamentais em diversas áreas da computação: Sistemas Operacionais Arquitetura de Computadores Compiladores Entre outros.
Como se familiarizar com seu banco de dados Como se familiarizar com seu banco de dados usando o SPSS Profa. Suzi Camey Depto de Estatística - UFRGS.
Análise Sintática Ascendente
Relações em uma Gramática
Análise Léxica Supondo o trecho de programa abaixo:
Construção de Compiladores
Construção de Compiladores
Construção de Compiladores
5.6 – Complementos de Yacc – Usando Yacc com gramáticas ambíguas
Análise léxica e sintática
Aula prática - análise contextual
Sintaxe e Semântica Prof.: Gláucya Carreiro Boechat
Análises léxica e sintática
Análise Léxica e Sintática
Prof. Alexandre Monteiro Recife
1 JavaCC Prof. Alexandre Monteiro Baseado em material cedido pelo Prof. Euclides Arcoverde Recife.
Geração de Código Intermediário
Esquemas de Tradução Prof. Alexandre Monteiro
Pilhas Profa. Nádia Félix.
Análise Léxica.
Capítulo II Gramáticas e Linguagens
Analise sintática aula-07-analise-sintática.pdf.
Esquemas L-atribuídos
Aulas 2 e 3 – Java – Prof. Marcelo Heitor # O método main e argumentos na linha de comando; # Fluxo padrão de entrada e saída; # A classe JOptionPane;
Análise Sintática LR Prof. Alexandre Monteiro
Arquitetura de Desenvolvimento Web MVC vs. Three Tiers
Análises léxica e sintática
Analise Semântica aula-10-analise-semântica.pdf.
Analisador Léxico Prof. Alexandre Monteiro
Projeto de Tradutor Preditivo. Introdução Introdução Esquemas L-atribuidos são reconhecidos por analisadores Top-Down. Entre estes analisadores, se encontra.
Aula prática 2 Operadores e Expressões Comandos de Decisão Comentários
Aula 12 1 Análise Sintáctica Compiladores, Aula Nº 12 João M. P. Cardoso.
Compiladores.
Análise Sintática Prof. Alexandre Monteiro
Análise Léxica Prof. Alexandre Monteiro
Tradução Dirigida por Sintaxe
Introdução à complexidade de algoritmos Luiz Gonzaga da Silveira Junior.
CES-41 COMPILADORES Aulas Práticas Capítulo II A Ferramenta Yacc.
Aula 18 1 Análise Sintáctica Compiladores, Aula Nº 18 João M. P. Cardoso.
Hibernate Mapeamento Objeto-Relacional Aula Prática I
Mas para que serve um analisador sintático? Verificar se a estrutura gramatical do programa está correta Escrito de outra forma: O texto segue as regras.
Aula Prática 1 Monitoria IP/CC (~if669) (A partir do slide elaborado por Luís Gabriel)
Revisão Compiladores – AP2
CES-41 COMPILADORES Aulas Práticas
Compiladores Análise Sintática
Revisão Compiladores – AP1
Faculdade Pernambucana - FAPE Setembro/2007
COMPILADORES 04 Prof. Marcos.
Recursividade Profs. De Prog2 e Lab2.
Algoritmos.
Computabilidade e Linguagens Formais
Sintaxe de uma Linguagem
tópicostópicos itens 01. Terminologia 02. Operações básicas 03. Representação de linguagens 04. Formalização de gramáticas 05. Processo de derivação 06.
Analisador sintático: Tipos de análises sintáticas
Compilador Software que traduz o texto (linguagem fonte) que representa um programa para código máquina(linguagem alvo) capaz de ser executado pelo.
Faculdade Pernambuca - FAPE Compiladores Abril/2007 Compiladores Abril/2007.
Questionário (Básico) Autor: Skyup Informática. Atividade - Questionário O módulo permite criar uma série de questões, que deverão ser respondida pelos.
Transcrição da apresentação:

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

Contatos Prof. Guilherme Alexandre Monteiro Reinaldo Apelido: Alexandre Cordel E-mail/gtalk: alexandrecordel@gmail.com greinaldo@fbv.edu.br Site: http://www.alexandrecordel.com.br/fbv Celular: (81) 9801-1878

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

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

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

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

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

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

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

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...

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()

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); }

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

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 ..> }

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”

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?

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(β)

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

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 }

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(...); }

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 }

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)

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

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 | ε

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

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

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

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

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!

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

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

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

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 | ε

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

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

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

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

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

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

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) { }

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 | ε

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! */ }

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

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?

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)

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...

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... }

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(); }

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 ...

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”

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

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

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

Algoritmos FIRST e FOLLOW Tabela LL(1) Euclides Arcoverde http://sites.google.com/site/euneto/

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

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

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 }

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

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

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) = { $, +, ), * }

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’) = { * }

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, $]

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)

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

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’ -> ε

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)