Baixe o app para aproveitar ainda mais
Prévia do material em texto
Paradigmas de Linguagem de Programação Análise Léxica e Sintática Tópicos Análise léxica. O problema da análise sintática: Construção da árvore de análise: • Análise cima-baixo. • Análise baixo-cima. Análise descendente recursiva. Introdução Os analisadores sintáticos são baseados em uma descrição formal da sintaxe do programa. O formalismo mais usual é a gramática livre de contexto ou BNF: Suas descrições de sintaxe de programa são claras e concisas, tanto para leitores humanos como para os sistemas de software que as utilizam. As implementações baseadas em BNF são relativamente fáceis de manter, devido a sua modularidade. Introdução Praticamente todos os compiladores separam a tarefa de análise sintática em duas partes distintas: Simplicidade: técnicas para análise léxica são menos complexas do que as exigidas para análise sintática. Eficiência: Separação facilita otimização seletiva (apenas na análise sintática). Portabilidade: analisador léxico lê arquivos de entrada contendo o programa (de certa forma dependente de plataforma). O analisador sintático pode ser independente da plataforma. Introdução Análise léxica – trata as construções de pequena escala: nomes e literais numéricos. Análise sintática propriamente dita – trata as construções de larga escala: expressões, instruções e unidades de programa. Resultados Unidades léxicas Árvores de análise Código intermediário Analisador sintático Gerador de código intermediário Computador Tabela de símbolos Analisador léxico Gerador de código Otimização Programa Fonte Linguagem de máquina Dados de entrada O processo da compilação Análise léxica Análise léxica Um analisador léxico é uma ferramenta de casamento de padrões: Tenta encontrar subcadeia de uma cadeia de caracteres que casa com o padrão dado. Um programa de entrada para um compilador aparece como uma única cadeia de caracteres → o analisador léxico coleta caracteres em grupamentos lógicos (lexemas) e atribui códigos internos (símbolos/tokens) aos grupamentos de acordo com sua estrutura. Análise léxica Lexemas – grupamentos de caracteres. Símbolos (tokens) – códigos internos. Ex: Soma = SomaVelha – Valor / 100; Símbolo Lexema Identificador Soma Op_atribuição = Identificador SomaVelha Op_subtração - Identificador Valor Op_divisão / Literal_int 100 PontoeVírgula ; Análise léxica O processo do analisador léxico inclui: Saltar comentários e brancos fora dos lexemas (são irrelevantes para o significado do programa). Inserção dos lexemas de nomes definidos pelo programador na tabela de símbolos. Detectar erros de sintaxe dos símbolos e reportar esses erros ao usuário. O analisador léxico produz o próximo lexema e seu símbolo associado e retorna para o analisador sintático: O analisador sintático tem como única visão do programa a saída do analisador léxico, um lexema por vez. Análise léxica Exemplo: Analisador léxico que reconheça apenas nomes de programas, palavras reservadas e literais inteiros. Construção do analisador léxico: Diagrama de estados: • Estados e transições para cada padrão de símbolos. Código. Análise léxica AcrescentaCaracter; ObtemCaracter; id int início retorna pesquisa(lexema) retorna Lit_Int Letra Digito AcrescentaCaracter; ObtemCaracter; AcrescentaCaracter; ObtemCaracter; Letra/Digito AcrescentaCaracter; ObtemCaracter; Digito Subprogramas utilitários: ObtemCaracter: pega o próximo caractere do programa fonte e coloca na variável ProximoCaracter. Determina a classe a que o caractere pertence e coloca-o na variável ClasseCaracter. AcrescentaCaracter: coloca o caractere do ProximoCaracter na variável lexema. Pesquisa: determina se o lexema é uma palavra reservada ou um nome (retorna zero). Diagrama de estados para reconhecer nomes, palavras reservadas e literais inteiros Análise léxica /* lex - um analisador léxico simples */ int lex(){ ObtemCaracter(); switch(ClasseCaracter){ /* analisa identificadores e palavras reservadas */ case LETRA: AcrescentaCaracter(); ObtemCaracter(); while (ClasseCaracter == LETRA || ClasseCaracter == DIGITO){ AcrescentaCaracter(); ObtemCaracter(); } return pesquisa(lexema); break; Análise léxica /* lex - um analisador léxico simples */ /* analisa literais inteiros */ case DIGITO: AcrescentaCaracter(); ObtemCaracter(); while (ClasseCaracter == DIGITO){ AcrescentaCaracter(); ObtemCaracter(); } return LIT_INT; break; } /* Fim do switch*/ } /* Fim da função lex*/ Exercício 1 Faça um diagrama de estados que reconheça uma forma de comentários das linguagens baseadas no C, aquela que começa com /* e termina com */ Solução 1 ObtemCaracter; corpo com início /* ObtemCaracter; Letra/Digito */ Letra/Digito Subprogramas utilitários: ObtemCaracter: pega o próximo caractere do programa fonte e coloca na variável ProximoCaracter. Determina a classe a que o caractere pertence e colocá-la na variável ClasseCaracter. AcrescentaCaracter: coloca o caractere do ProximoCaracter na variável lexema. Pesquisa: determina se o lexema é uma palavra reservada ou um nome (retorna zero). n_comObtemCaracter; Exercício 2 Faça um diagrama de estados para reconhecer literais de números reais em C. Solução 2 id int início retorna Lit_int retorna Lit_real Letra Digito AcrescentaCaracter; ObtemCaracter; AcrescentaCaracter; ObtemCaracter; Digito . (ponto) AcrescentaCaracter; ObtemCaracter; real AcrescentaCaracter; ObtemCaracter; ObtemCaracter; Digito Análise sintática Análise sintática Os analisadores de LP constroem árvores de análise para os programas. A informação necessária para construir a árvore é criada durante o processo de análise. As árvores de análise e as derivações incluem toda a informação sintática necessária para um processador de linguagem. Metas: Verificar se programa de entrada está sintaticamente correto. • Quando erro é encontrado, deve produzir mensagem de diagnóstico. Produzir uma árvore de análise (completa ou esboçar a sua estrutura). O resultado da análise sintática é usado como base para a tradução. Análise sintática Os analisadores são classificados de acordo com a direção na qual eles constroem a árvore de análise: Cima-baixo (top-down) Baixo-cima (bottom-up) Os analisadores operam sob a obrigação de que eles jamais olharão à frente mais do que um símbolo no programa de entrada. Analisador cima-baixo Analisador cima-baixo (top-down): Utiliza derivação mais à esquerda. Constrói árvore em pré-ordem. • Visitar nó da raiz. • Percorrer em pré-ordem subárvore esquerda. • Percorrer em pré-ordem subárvore direita. Percurso em pré-ordem A B C E F H IG D Percurso em pré-ordem: Visitar nó da raiz. Percorrer em pré-ordem subárvore esquerda. Percorrer em pré-ordem subárvore direita. A B D G C E H I F Analisador cima-baixo Dada uma forma sentencial encontrar a próxima derivação: Regras: A → bB, A → cBb e A → a Escolher entre as regras para gerar a próxima forma sentencial sob a restrição de um símbolo considerado à frente. A forma geral de uma forma sentencial à esquerda é xAα: x é uma cadeia de símbolos terminais. A é um não terminal. α é uma cadeia mista. A é o não terminal maisà esquerda, então deve ser expandido para se obter a próxima forma sentencial na derivação mais à esquerda. A próxima forma sentencial pode ser: xbBα ou xcBbα ou xaα Analisador baixo-cima Analisador baixo-cima (bottom-up): Produz o inverso da derivação mais à direita. • Produz a árvore das folhas para a raiz. Gramática: • S → Ax, A → a | b • Derivações para ax: • Cima-baixo: • S => Ax => ax • Baixo-cima: • a <= Ax <= S Análise descendente recursiva Análise descendente recursiva Um analisador descendente recursivo consiste de uma coleção de subprogramas, muitos dos quais são recursivos, e produz uma árvore de análise cima-baixo (descendente). Natureza das linguagens de programação → várias estruturas recursivas: Análise de estruturas recursivas das LPs leva naturalmente a regras gramaticais recursivas. A EBNF é muito adequada para analisadores descendentes recursivos. Dada a gramática apropriada, o analisador descendente recursivo é relativamente simples de implementar. EBNF EBNF – versão estendida da BNF: Não aumenta poder descritivo, mas aumenta sua legibilidade e capacidade de escrita. EBNF – parte opcional Parte opcional de um lado direito, definida por colchetes: Exemplo em C: EBNF: • <seleção> → if (<expressão>) <instrução> [else <instrução>]; BNF: • <seleção> → if (<expressão>) <instrução>; • <seleção> → if (<expressão>) <instrução> else <instrução>; EBNF – repetição Indicar que o lado direito pode ser repetido indefinidamente ou omitido completamente, definido por chaves. Permite que listas sejam criadas com uma única regra, em vez de usar recursão e duas regras. Exemplo: EBNF : • <list_ident> → <identificador> {, <identificador>} BNF: • <list_ident> → <identificador> • <list_ident> → <identificador>, <list_ident> EBNF – múltipla escolha Opções de múltipla escolha: quando um único elemento deve ser escolhido de um grupo, as opções são colocadas entre parênteses e separadas por OU ‘|’. Exemplo em Pascal: • EBNF: • <inst_for> → for <var> := <expr> (to|downto) <expr> do <inst> • BNF: • <inst_for> → for <var> := <expr> to <expr> do <inst> • <inst_for> → for <var> := <expr> downto <expr> do <inst> Análise descendente recursiva Um analisador descendente recursivo tem um subprograma para cada não-terminal da gramática. Cada subprograma deve: Dada uma cadeia de entrada, investigar a árvore que pode ter como raiz o não-terminal e cujas folhas casam com a cadeia de entrada. Assumiremos que cada não-terminal possui uma única regra, possivelmente com múltiplos LD (lados direitos) separados por OU. Análise descendente recursiva Analisador sintático (descendente recursivo): Um subprograma para uma regra com um único LD é relativamente simples. Para cada símbolo terminal no LD é feita uma comparação com proximoSimbolo: • Se eles não casam → erro de sintaxe. • Se eles casam → analisador léxico é chamado para obter o próximo símbolo de entrada. Para cada não-terminal o subprograma de análise correspondente é chamado. Analisador léxico: Função lex() Obtém o próximo lexema e coloca seu código de símbolo na variável proximoSimbolo: • Ex: Codigo_Mais (constante nomeada para o símbolo +). Análise descendente recursiva Expressões aritméticas simples: <expr> → <termo> {(+ | -) <termo> } <termo> → <fator> {(*|/) <fator>} <fator> → ident | (<expr>) Análise descendente recursiva /* função expr – Analisa cadeias na linguagem gerada pela regra: <expr> → <termo> {(+ | -) <termo>} */ void expr(){ /* Analisa o primeiro termo */ termo(); /* Enquanto o próximo símbolo for + ou -, chama lex para obter o próximo símbolo e analisa o próximo termo*/ while (proximoSimbolo == Codigo_Mais || proximoSimbolo == Codigo_Menos){ lex(); termo(); } } /* Fim da função expr*/ /* função fator – Analisa cadeias na linguagem gerada pela regra: <fator> → ident | (<expr>) */ void fator(){ /* Determina qual LD */ if (proximoSimbolo == codigo_ident) lex(); /* Se o LD (<expr>)*/ else if (proximoSimbolo == parent_esq){ lex(); expr(); if (proximoSimbolo == parent_dir) lex(); else erro(); } else erro(); } /* Fim da função fator*/ Análise descendente recursiva Exercício Escreva o subprograma descendente recursivo para a regra de termo: <termo> → <fator> {(*|/) <fator>} Solução /* função termo – Analisa cadeias na linguagem gerada pela regra: <termo> → <fator> {(* | /) <fator>} */ void termo(){ /* Analisa o primeiro fator */ fator(); /* Enquanto o próximo símbolo for * ou /, chama lex para obter o próximo símbolo e analisa o próximo fator*/ while (proximoSimbolo == Codigo_Vezes || proximoSimbolo == Codigo_Div){ lex(); fator(); } } /* Fim da função termo*/ Análise descendente recursiva Um analisador descendente recursivo pode ser facilmente escrito se uma gramática apropriada estiver disponível para a linguagem. Análise sintática Uma característica simples das gramáticas que causa um problema catastrófico para os analisadores cima-baixo é a recursão à esquerda: • Direta: • A → A + B • Indireta: • A → BaA, B → Ab É preciso evitar a inclusão de recursão à esquerda nas regras da gramática. A chama a si mesmo, que chama a si mesmo de novo, que ... não leva a lugar nenhum!! Análise sintática O analisador cima-baixo realiza a escolha do LD baseado no próximo símbolo de entrada. Exemplo: <seleção> → if (<expressão>) <instrução>; | if (<expressão>) <instrução>; else <instrução>; Conhece apenas o símbolo terminal if. Como escolher entre as duas regras? Análise sintática Existe um teste de gramáticas recursivas não à esquerda que indica se a escolha pode ser feita – teste de disjunção paritária: Consiste em determinar um conjunto de símbolos terminais, chamado primeiro, com base nos LDs de um dado símbolo não terminal. Para cada não terminal, A, na gramática que tenha mais de um LD: • Para cada par de regras, A → αi e A → αj, • Deve ser constatado que primeiro(αi) ∩ primeiro(αj) = ø. Análise sintática Exemplos de teste de disjunção paritária: • A → aB | bAb | c • Conjunto dos primeiros: {a}, {b} e {c} que são disjuntos. • Essas regras passam no teste. • A → aB | aAb • Conjunto dos primeiror: {a} e {a} que não são disjuntos. • Essas regras não passam no teste. Análise sintática Em muitos casos, a gramática que não passa pelo teste da disjunção paritária pode ser modificada através do processo chamado fatoração à esquerda. Exemplo: Antes da fatoração: <seleção> → if (<expressão>) <instrução>; | if (<expressão>) <instrução>; else <instrução>; Após fatoração (passa no teste de disjunção paritária): <seleção> → if (<expressão>) <instrução>; <nova> <nova> → є | else <instrução>; Exercício 1 Para as seguintes regras gramaticais, faça o teste da disjunção paritária. a) A → aB | bA | aBb b) A → aaA | b | caB Solução 1 a) A → aB | bA | aBb primeiro(aB) = {a}, primeiro(bA) = {b} , primeiro(aBb) = {a} {a} ∩ {b} ∩ {a} = {a} ≠ ø Não passa no teste a) A → aaA | b | caB primeiro(aaA) = {a}, primeiro(b) = {b}, primeiro(caB) = {c} {a} ∩ {b} ∩ {c} = ø Passa no teste Exercício 2 Escreva um subprograma descendente recursivo que analisa a linguagem gerada pelas regras: A → aaA | b | caB Solução 2 /* função A – Analisa cadeias na linguagemgerada pela regra: A → aaA | b | caB */ void A(){ /* Determina qual LD */ if (proximoSimbolo == codigo_a){ lex(); if (proximoSimbolo == codigo_a){ lex(); A(); } else erro(); } else if (proximoSimbolo == codigo_b) lex(); Solução 2 /* função A – Analisa cadeias na linguagem gerada pela regra: A → aaA | b | caB */ else if (proximoSimbolo == codigo_c){ lex(); if (proximoSimbolo == codigo_a){ lex(); B(); } else erro(); } else erro(); } /* Fim da função A*/ Referências Bibliográficas Sebesta, R. Conceitos de Linguagens de programação. 5ª edição. Bookman, 2003. Cap 4.
Compartilhar