Compiladores: P ASCAL jr

Documentos relacionados
Tokens, Padroes e Lexemas

Compiladores. Análise Sintática

Compiladores. Exemplo. Caraterísticas de Gramáticas. A αβ 1 αβ 2. A αx X β 1 β 2. Lembrando... Gramáticas Livres de Contexto

Introdução à Programação

COMPILADORES. Análise sintática. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática

Um Compilador Simples. Definição de uma Linguagem. Estrutura de Vanguarda. Gramática Livre de Contexto. Exemplo 1

Compiladores Analisador Sintático. Prof. Antonio Felicio Netto Ciência da Computação

Compiladores. Análise Sintática

V Análise Sintática. V.1.1 Gramáticas Livres de Contexto Definições de GLC

Compiladores. Motivação. Tradutores. Motivação. Tipos de Tradutores. Tipos de Tradutores

V Teoria de Parsing. Termos Básicos: Parser Analisador Sintático Parsing Analise Sintática Parse Representação da analise efetuada

Análise Sintática. Fabiano Baldo

Linguagens Livres de Contexto

Plano da aula. Compiladores. Os erros típicos são sintáticos. Análise Sintática. Usando Gramáticas. Os erros típicos são sintáticos

II.1 Conceitos Fundamentais. Uma delas é programar o =>

COMPILADORES. Análise sintática. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática

INE5416 Paradigmas de Programação. Ricardo Azambuja Silveira INE CTC UFSC E Mail: URL:

CAP. VI ANÁLISE SEMÂNTICA

Analisadores Descendentes Tabulares; Cjs First Follow

LINGUAGEM LIVRE DE CONTEXTO GRAMÁTICA LIVRE DE CONTEXTO

FACULDADE LEÃO SAMPAIO

Análise Sintática I. Eduardo Ferreira dos Santos. Abril, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 42

Compiladores Aula 4. Celso Olivete Júnior.

Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES. Introdução. Geovane Griesang

Prof. Adriano Maranhão COMPILADORES

Compiladores. Introdução

LFA Aula 09. Gramáticas e Linguagens Livres de Contexto 18/01/2016. Linguagens Formais e Autômatos. Celso Olivete Júnior.

V.2 Especificação Sintática de Linguagens de Programação

Sintaxe e Semântica. George Darmiton da Cunha Cavalcanti.

Análise sintática. Prof. Thiago A. S. Pardo. Análise sintática ascendente

Gramáticas Livres de Contexto Parte 1

Compiladores - Análise Ascendente

Compiladores. Introdução à Compiladores

Compiladores - Análise Ascendente

Linguagens Livres do Contexto. Adaptado de H. Brandão

Compiladores I Prof. Ricardo Santos (cap 3 Análise Léxica: Introdução, Revisão LFA)

Compiladores. Análise Léxica

INE5421 LINGUAGENS FORMAIS E COMPILADORES

Compiladores I Prof. Ricardo Santos (cap 1)

Análise sintática. Análise sintática ascendente. Parte-se dos símbolos terminais em direção ao símbolo inicial da gramática. Derivação mais à direita

Compiladores. Fabio Mascarenhas

Análise sintática. Questão. E se a análise sintática pudesse ser modelada por autômatos?

Vantagens de uma Gramática. Sintaxe de uma Linguagem. Analisador Sintático - Parser. Papel do Analisador Sintático. Tiposde Parsers para Gramáticas

Análise Sintática Introdução

CP Compiladores I Prof. Msc.. Carlos de Salles

FERRAMENTA DE AUXÍLIO AO PROCESSO DE DESENVOLVIMENTO DE SOFTWARE INTEGRANDO TECNOLOGIAS OTIMIZADORAS

Construção de Compiladores Aula 16 - Análise Sintática

Compiladores Aula 6. Celso Olivete Júnior.

Sintaxe do Pascal Simplificado Estendido de 12 novas construções em Notação EBNF (BNF estendida)

INE5318 Construção de Compiladores. AULA 4: Análise Sintática

Programação Introdução

COMPILADORES. Análise sintática. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática

LINGUAGENS FORMAIS Definições. Desenvolveram-se na História em função da necessidade dos grupos humanos que as empregavam

Análise Sintática Bottom-up

Compiladores. Análise lexical. Plano da aula. Motivação para análise lexical. Vocabulário básico. Estrutura de um compilador

Compiladores. Prof. Bruno Moreno Aula 8 02/05/2011

Algoritmos. Algoritmos e Linguagem de Programação - Prof Carlos Vetorazzi

Compiladores. Análise Léxica

Autômatos e Linguagens

INE5317 Linguagens Formais e Compiladores. AULA 10: Anális e S intática

Linguagens de Programação Aula 4

Análise Sintática. Eduardo Ferreira dos Santos. Outubro, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 18

Análise Sintática Descendente

15/03/2018. Professor Ariel da Silva Dias Aspectos sintáticos e semânticos básicos de linguagens de programação

Introdução. Compiladores Análise Semântica. Introdução. Introdução. Introdução. Introdução 11/3/2008

Projeto de Compiladores

Análise Sintática LL(1)

Compiladores - Gramáticas

Concurso Público para provimento de cargo efetivo de Docentes. Edital 20/2015 CIÊNCIA DA COMPUTAÇÃO II Campus Rio Pomba

Compiladores. Bruno Lopes. Bruno Lopes Compiladores 1 / 32. Instituto de C

Universidade Federal de Alfenas

Compiladores 02 Analise léxica

Universidade Estadual da Paraíba - UEPB Curso de Licenciatura em Computação

DESENVOLVIMENTO DO COMPILADOR PARA A LINGUAGEM SIMPLE

Universidade Federal de Goiás Bacharelado em Ciências da Computacão Compiladores

Compiladores - Gramáticas

Projeto de Compiladores

Tratamento dos Erros de Sintaxe. Adriano Maranhão

BNF (Backus-Naur Form) Gramática Livres de Contexto / Estruturas Recursivas

Conceitos de Linguagens de Programação

ACH2043 INTRODUÇÃO À TEORIA DA COMPUTAÇÃO

Compiladores. Parser LL 10/13/2008

Eduardo Belo de Araújo. Analisador ANSI-C

Capítulo 7. Expressões e Sentenças de Atribuição

Compiladores. Bruno Lopes. Bruno Lopes Compiladores 1 / 31. Instituto de C

TÉCNICO DE INFORMÁTICA - SISTEMAS

Apêndice A. Pseudo-Linguagem

Introdução aos Compiladores

Paradigmas de Linguagem de Programação

Apresentação. !! Familiarização com os métodos de construção de compiladores de linguagens e com as técnicas de compilação mais habituais.

Linguagens Formais e Autômatos. Simplificação de Gramáticas Livre do Contexto (GLC)

Linguagens de Programação Aula 3

Compiladores - Análise Léxica

Compiladores - Especificando Sintaxe

Linguagens Formais e Autômatos

IV Gramáticas Livres de Contexto

Compilação: Erros. Detecção de Erros: * Analisadores Top-Down - Preditivo Tabular (LL) - Feito a mão. * Analisadores Botton-Up: - Shift-Reduce (SLR)

Transcrição:

Compiladores: P ASCAL jr Rogério Eduardo da Silva, M.Sc. 2005/2

Sumário 1 Introdução 1 1.1 Evolução das Linguagens de Programação.................. 1 1.2 Introdução à Compilação............................ 2 1.2.1 Fases da Compilação.......................... 3 1.3 Ferramentas para Geração de Compiladores................. 6 2 Um Compilador Simples de uma Passagem 7 2.1 Definição da Sintaxe.............................. 7 2.2 Análise Gramatical............................... 8 2.2.1 Exercícios Propostos.......................... 9 2.3 Características da linguagem P ASCAL jr................... 10 2.3.1 Exercícios Propostos.......................... 12 3 Análise Léxica 13 3.1 O Papel do Analisador Léxico......................... 13 3.2 Buferização de Entrada............................. 14 3.3 Gramáticas e Linguagens Regulares...................... 15 3.3.1 Exercícios Propostos.......................... 16 3.4 Especificação e Reconhecimento de Tokens.................. 17 3.4.1 Trabalho Prático #1.......................... 19 4 Análise Sintática 21 4.1 O Papel do Analisador Sintático........................ 21 4.2 Análise Sintática Ascendente - BOTTOM UP................ 23 4.2.1 Algoritmo Empilhar-e-Reduzir................... 23 4.3 Análise Sintática Descendente - TOP DOWN................ 24 4.3.1 Análise Sintática Preditiva....................... 25 4.3.2 Exercícios Propostos.......................... 26 4.4 Reconhecedor de Gramáticas Preditivas Descendentes............ 27 4.4.1 Algoritmo para Construção da Tabela de Análise.......... 29 4.4.2 Projeto de uma Gramática para um Analisador Sintático Preditivo Ascendente............................... 30 4.4.3 Projeto de uma Gramática para um Analisador Sintático Preditivo Descendente............................... 31 4.4.4 Exercícios Propostos.......................... 38 4.4.5 Trabalho Prático #2.......................... 38 i

5 Análise Semântica 41 5.1 Tradução Dirigida pela Sintaxe........................ 41 5.1.1 Definições L-Atribuídas......................... 43 5.1.2 Verificações de Contexto........................ 44 5.2 Tabela de Símbolos............................... 46 5.2.1 Atributos dos Nomes dos Identificadores............... 47 5.2.2 Hashing................................. 47 5.3 Projeto das Regras Semânticas......................... 50 5.3.1 Trabalho Prático #3.......................... 56 6 Geração de Código Intermediário 59 6.1 Linguagens Intermediárias........................... 59 6.1.1 Representações Gráficas........................ 59 6.1.2 Notação Pós (e Pré) Fixadas...................... 60 6.1.3 Código de Três-Endereços....................... 61 6.2 BackPatching (Retrocorreção)......................... 64 7 Otimização de Código 67 7.1 Otimização Peephole.............................. 67 7.2 Otimização de Blocos Sequenciais através de grafos............. 68 7.2.1 Algoritmo para Construir o GAD de um bloco............ 69 7.2.2 Algoritmo para Ordenação de um GAD................ 70 8 Geração de Código Objeto 71 8.1 Máquina Objeto................................. 72 8.1.1 Regras para Geração de Código Objeto................ 76 8.1.2 Trabalho Prático #4.......................... 84 ii

Lista de Figuras 1.1 Processo de Compilação............................ 2 1.2 Fases da Compilação.............................. 3 1.3 Árvore resultante da análise de um comando de atribuição em PASCAL.. 4 2.1 Representação da árvore gramatical da produção A XYZ......... 8 2.2 Ambigüidade Gramatical............................ 8 3.1 O papel do analisador léxico.......................... 13 3.2 Buffer de entrada para um analisador léxico................. 15 3.3 Autômato finito de reconhecimento de números inteiros e reais....... 17 3.4 AFD de reconhecimento de identificadores simples.............. 18 3.5 AFD de reconhecimento de strings...................... 18 4.1 Exemplo de Árvore Sintática.......................... 22 4.2 Derivação à Esquerda e à Direita....................... 22 4.3 Análise descendente com backtracking.................... 25 4.4 Exemplos de Recursão à Esquerda e à Direita................ 27 4.5 Funcionamento de um Analisador Sintático Descendente.......... 28 5.1 Exemplo de Árvore Decorada para a Expressão 3*5+4........... 42 5.2 Grafo de Dependências............................. 43 5.3 Tipos Simples e Construtor de Tipos..................... 44 5.4 Hashing com Encadeamento.......................... 49 6.1 Exemplo de Representação Gráfica de Operadores para a=b*c+b*2.... 60 6.2 Backpatching para expressões lógicas..................... 65 7.1 Grafo Acíclico Dirigido - GAD......................... 70 iii

Capítulo 1 Introdução Entende-se por linguagem como uma forma eficiente de comunicação entre pessoas. Na verdade a linguagem é um conjunto de palavras usadas, segundo certas regras, para a formação de frases compreensíveis por ambos os interlocutores (falantes). Quando um dos interlocutores é o computador, se faz necessário o uso de uma linguagem especial denominada linguagem de programação que permite a comunicação entre homem e máquina através da definição de comandos. Uma L. P. é ser dita de baixo nível, se esta somente aceitar comandos na própria linguagem da máquina (0 s e 1 s) que é de difícil aplicação. Já as linguagens ditas de alto nível, são representadas por ações próximas ao problema a ser resolvido que são, posteriormente, traduzidas para a linguagem de máquina, através de um agente especial denominado compilador ou interpretador. Concluindo: compilador é um programa capaz de traduzir um certo programa fonte (escrito em uma linguagem fonte) para outro programa objeto (escrito em uma linguagem objeto) geralmente a própria linguagem de máquina. 1.1 Evolução das Linguagens de Programação Cronologicamente, as L. P. s são classificadas em cinco gerações: (1 a ) linguagens de máquina; (2 a ) linguagens simbólicas (Assembly); (3 a ) linguagens orientadas ao usuário; (4 a ) linguagens orientadas à aplicação e (5 a ) linguagens de conhecimento. As duas primeiras são consideradas linguagens de baixo nível, enquanto que as demais de alto nível. Os primeiros computadores só podiam ser programados através da sua própria linguagem de máquina (código binário), onde cada operação possuía sua representação binária que era passada à máquina através de circuitos elétricos. Esse processo, além de extremamente difícil e cansativo, era altamente sujeito a erros devido a sua grande complexidade de execução. A seguir, como uma primeira tentativa de simplificação, surgem as linguagens simbólicas ou de montagem (Assembly). Agora, extensas seqüências binárias são substituídas por mnemônicos que são palavras especiais que representam certas ações básicas. Exemplo MOV, JMP, etc. Os mnemônicos precisavam ser traduzidos para a linguagem de máquina antes da sua execução. 1

A 3 a geração surgiu na década de 60, com as linguagens procedimentais como FOR- TRAN, PASCAL e ALGOL e declarativas como LISP e PROLOG. Nas linguagens procedimentais, um programa especifica uma seqüência de passos a serem seguidos para a solução do problema. Já as linguagens declarativas são subdivididas em funcionais e lógicas. A programação funcional se baseia na teoria das funções recursivas, enquanto que, as linguagens lógicas se baseiam em proposições da lógica de predicados (fatos e regras). Devido ao fato de programas escritos em linguagens de 3 a geração serem muito extensos e de difícil manutenção, surgiram as linguagens de aplicação (4 a geração), onde o desenvolvedor deixa de se preocupar com atividades secundárias e trata apenas da codificação do problema (foco do programador deixa de ser a codificação para ser a análise do problema). Aspectos como: interface de entrada e saída, relatórios, etc. são resolvidos pela própria linguagem através de um banco de dados e dicionários associados às aplicações desenvolvidas. A 5 a geração das linguagens de programação atua em problemas altamente complexos onde a representação de conhecimento se faz necessária para sua solução, como os problemas enfrentados pela inteligência artificial. A linguagem PROLOG é aceita como pertencente a esta geração. 1.2 Introdução à Compilação Conforme já dito, um compilador nada mais é do que um programa tradutor responsável por converter uma certa linguagem fonte em outra linguagem objeto (ver Figura 1.1). Usualmente a linguagem objeto é a própria linguagem de máquina, mas não necessariamente. Programa Fonte COMPILADOR Programa Objeto Mensagem de Erro Figura 1.1: Processo de Compilação Existem dois tipos básicos de tradutores: os compiladores e os interpretadores. Os primeiros fazem uma análise completa sobre o programa fonte, caso não encontre erros faz a tradução de todo o código fonte para a linguagem objeto que será posteriormente executado em uma máquina capaz de fazê-lo. Já os interpretadores não têm essa preocupação holística (análise completa) sobre o programa fonte. Um interpretador traduz um comando fonte por vez e o executa em uma máquina virtual (programa que simula o funcionamento de um computador) sem a necessidade da criação do programa objeto. Interpretadores são mais simples de serem implementados, porém, compiladores geram execuções mais rápidas de programas, pois não há a perda de tempo de traduções virtuais a cada nova instrução executada. 2

1.2.1 Fases da Compilação O processo de compilação pode ser dividido em dois grupos de etapas: as etapas de análise e as etapas de síntese. Na análise, o programa fonte é percorrido em busca de erros de programação (inconsistências com a linguagem fonte), já na etapa de síntese (após a verificação da corretude do programa de origem), efetua-se a tradução, propriamente dita, do código fonte para a linguagem objeto em questão. A figura 1.2 abaixo ilustra todo o processo: Programa Fonte Análise Léxica Análise Sintática ANÁLISE Tabela de Símbolos Análise Semântica Geração de Código Intermediário Manipulador de Erros Otimização de Código SÍNTESE Geração de Código Objeto Programa Objeto Figura 1.2: Fases da Compilação A análise léxica ou scanning é a primeira etapa do processo de compilação. Ela é responsável por analisar linearmente os caracteres do programa fonte e agrupá-los em unidades léxicas denominadas tokens. O token é o elemento mais básico da programação; ele é representado por um conjunto de caracteres que apresentam um significado claro para o programa. Exemplo: Para o seguinte código em PASCAL: Media := Nota1 + Nota2 * 2 Os caracteres poderiam ser agrupados da seguinte forma: 1. O identificador Media 2. O símbolo de atribuição := 3. O identificador Nota1 4. O sinal de adição + 5. O identificador Nota2 6. O sinal de multiplicação * 3

7. O número 2 Os espaços em branco presentes na sentença são ignorados durante a análise. O resultado da análise léxica é uma lista contendo todos os tokens encontrados no programa fonte. Essa lista léxica é então o elemento de entrada para a análise sintática ou análise gramatical (parsing), onde é verificado se os tokens podem ser agrupados em sentenças válidas (comandos, expressões, etc.) da linguagem fonte. Normalmente, esses agrupamentos são realizados através da construção de uma árvore sintática conforme é apresentado na figura 1.3: Comando de Atribuição Identificador Símbolo de Atribuição Expressão Media := Expressão Operador Aritmético Expressão Nota1 + Expressão Operador Aritmético Expressão Nota2 * 2 Figura 1.3: Árvore resultante da análise de um comando de atribuição em PASCAL A estrutura hierárquica de um programa é usualmente expressa por regras recursivas. Por exemplo, poderíamos ter as seguintes regras como parte definição de expressões: 1. Qualquer identificador é uma expressão 2. Qualquer número é uma expressão 3. Se expressão 1 e expressão 2 são expressões válidas, então expressão 1 op. aritmético expressão 2 também é A estrutura utilizada para a representação dessas regras é a gramática livre de contexto (GLC), normalmente apresentada na Forma Normal de Backus (BNF). Exemplo: comando ::= while atribuição... while ::= while expr bool do comando atribuição ::= identificador := expr aritm expr bool ::= expr aritm op.lógico expr aritm expr aritm ::= expr aritm op.aritm termo termo termo ::= número identificador Após a análise sintática, tem-se a certeza de que o programa está escrito corretamente (respeita as regras gramaticais da linguagem fonte), porém, será que o programa escrito faz algum sentido? Ou seja, executa de forma apropriada? 4

A análise semântica tem por objetivo validar os comandos e expressões através de análises como compatibilidade de tipos e escopo de identificadores. Esta etapa analisa, por exemplo, se um identificador declarado como variável é usado como tal, ou se uma expressão atribuída a uma variável retorna um tipo compatível com o qual foi declarada a variável (em algumas linguagens, uma variável inteira não pode receber uma expressão real). Até aqui foi realizada a etapa de análise do programa fonte, ou seja, a procura por erros de programação. Caso nenhum erro seja encontrado, o processo de compilação passa então para a etapa de síntese, ou seja, a construção do programa objeto. A geração do código intermediário é a primeira fase da construção do programa objeto. O que ela faz é a representação do programa fonte em uma linguagem intermediária simplificada (máquina abstrata), o que permite a realização da próxima etapa mais facilmente. A próxima etapa é a otimização de código, que tem por objetivo tentar modificar o código intermediário no intuito de melhorar a velocidade de execução, bem como a utilização do espaço de memória, fazendo com isso, um uso mais racional dos recursos da máquina. A última etapa do processo de compilação é a geração de código objeto propriamente dita. Esta fase tem como objetivos: produção de código objeto, reserva de memória para constantes e variáveis, seleção de registradores, etc. É a fase mais difícil, pois requer uma seleção cuidadosa das instruções e dos registradores da máquina alvo a fim de produzir código objeto eficiente. Exemplo de geração de código para o código fonte: While I < 100 do I := J + I Código Intermediário Otimização Código Objeto L0: if I<100 goto L1 L0: if I 100 goto L2 L0: MOV AX, I goto L2 I := J+I CMP AX, 100 goto L0 JGE L2 MOV AX, J MOV BX, I ADD BX MOV I, AX JMP L0 L1: Temp := J+I L2:... L2:... I := Temp goto L0 L2:... Além dessas fases, há também os módulos de gerenciamento de tabelas e manipulação de erros. O gerenciamento de tabelas consiste de um conjunto de tabelas e rotinas associadas que são utilizadas por quase todas as fases do tradutor. A principal estrutura deste módulo é a Tabela de Símbolos, que é responsável por armazenar informações acerca dos identificadores do programa sob análise, como por exemplo: declaração das variáveis, procedimentos e sub-rotinas, lista de parâmetros, etc.. 5

Os dados a serem armazenados dependem do projeto do tradutor, mas os mais comuns são: identificador, classe (variável, parâmetro, procedimento, etc.), tipo, endereço, tamanho. A tabela de símbolos deve ser estruturada de uma forma que permita rápida inserção e extração de informações, porém deve ser tão compacta quanto possível. O módulo de manipulação de erros tem por objetivo tratar os erros que são detectados em todas as fases de análise do programa fonte e deve dispor de mecanismos (recuperação de erros) que permitam que o processo de análise prossiga mesmo que erros tenham sido detectados. 1.3 Ferramentas para Geração de Compiladores Existem diversas ferramentas para auxiliar a construção de compiladores chamadas de geradores de compiladores ou sistemas de escritas de tradutores. A seguir são apresentados alguns exemplos: Geradores de Analisadores Gramaticais responsáveis por desenvolver analisadores sintáticos, normalmente a partir de entrada baseada numa gramática livre de contexto. Geradores de Analisadores Léxicos geram automaticamente analisadores léxicos a partir de uma especificação baseada em expressões regulares. Dispositivos de tradução dirigida pela sintaxe produzem coleções de rotinas que percorrem uma árvore gramatical, gerando código intermediário. Geradores automáticos de código tal ferramenta toma uma coleção de regras que definem a tradução de cada operação da linguagem intermediária para linguagem alvo. Tais regras precisam incluir detalhamento suficiente para que possamos lidar com os diferentes métodos de acesso possíveis para os dados. Dispositivos de fluxo de dados Ferramentas que auxiliam na etapa de otimização de código. Não é de escopo desta disciplina o estudo de ferramentas de implementação de compiladores, mais detalhes podem ser obtidos na bibliografia de apoio. 6

Capítulo 2 Um Compilador Simples de uma Passagem 2.1 Definição da Sintaxe A especificação da sintaxe de uma linguagem de programação pode ser obtida através de uma gramática livre de contexto. Exemplo: Seja o comando condicional da forma: IF Expressão THEN Comando ELSE Comando se Expr denotar a construção de uma expressão e Cmd denotar um comando (ou enunciado), pode-se usar as regras de produção de uma GLC 1 para representar tal estrutura da seguinte forma: < Cmd > IF < Expr > THEN < Cmd > ELSE < Cmd > as palavras-chave como IF, THEN e ELSE representam os símbolos terminais, enquanto que os termos Cmd e Expr, representam os não-terminais. Exemplo de uma GLC simples para definir expressões aritméticas baseadas apenas em adição e subtração: < Lista > < Lista > + < Digito > < Lista > < Lista > < Digito > < Lista > < Digito > < Digito > 0 1 2 3 4 5 6 7 8 9 onde os símbolos 0 a 9 e + ou - são os elementos terminais, enquanto que Lista e Digito representam os não-terminais. Convencionalmente, o primeiro não-terminal representa o axioma da gramática. Expressões exemplo: 1+1, 3-6+9, 1+2+3+4+5+6 1 Gramática Livre de Contexto 7

2.2 Análise Gramatical A análise gramatical é feita através de derivações de cadeias a partir do axioma da gramática. Se um não-terminal A possui uma produção A XYZ então, uma árvore gramatical pode ter um nó rotulado de A com 3 filhos X, Y e Z da esquerda para a direita, conforme a figura 2.1. A X Y Z Figura 2.1: Representação da árvore gramatical da produção A XYZ Formalmente, segundo uma GLC, a árvore gramatical resultante apresenta as seguintes propriedades: A raiz é rotulada pelo símbolo de partida (axioma); Cada folha é rotulada por um terminal ou por ε; Cada nó interno é rotulado por um elemento não-terminal; Se A X 1 X 2... X n é uma produção então, algum nó interno da árvore será rotulado por A sendo X 1 X 2... X n os rótulos dos filhos desse nó. Ambiguidade Uma gramática pode ter mais de uma árvore gramatical gerando uma dada cadeia, neste caso, ela é dita ser ambígua. Ambos os exemplos da figura 2.2 geram a sentença 9-5+2. Cadeia Cadeia Cadeia + Cadeia Cadeia - Cadeia Cadeia - Cadeia 2 9 Cadeia + Cadeia 9 5 5 2 Figura 2.2: Ambigüidade Gramatical 8

Associatividade de Operadores Convencionalmente, 9+5+2 é equivalente à (9+5)+2, pois, ao analisarmos o operando 5 precisamos decidir qual operação será realizada primeiro. Pela convenção da matemática a adição é associativa à esquerda, sendo assim o resultado (9+5)+2 é obtido. Na maioria das linguagens de programação, as quatro operações básicas (adição, subtração, multiplicação e divisão) são associativas à esquerda. A exponenciação é um exemplo de operador associativo à direita (em Fortran) 5**2**3 é equivalente a 5**(2**3). Outro exemplo é o operador de atribuição, onde a expressão a=b=c (em linguagem C) é tratada como a=(b=c). Precedência de Operadores Considere a expressão 9+5 2. Existem duas interpretações possíveis: (9+5) 2 e 9+(5 2). Quando mais de um tipo de operadores estiverem presentes em uma expressão é necessário se definir a ordem de precedência entre eles. Na aritmética, os operadores e / tem precedência mais alta do que + e -; assim, na expressão anterior o operador de multiplicação é capturado antes da adição. 2.2.1 Exercícios Propostos 1. Prova, através da construção da árvore de derivação, que os exemplos anteriores são válidos para a gramática de expressões aritméticas vista. 2. Considere a gramática livre de contexto: S SS+ SS a (a) Mostre que a cadeia aa+a pode ser gerada por esta gramática. (b) Construa a árvore gramatical para esta cadeia. (c) Qual é a linguagem gerada por esta gramática? Justifique sua resposta. 3. Quais são as linguagens geradas pelas seguintes gramáticas? (a) S 0S1 01 (b) S +SS -SS a (c) S S(S)S ε (d) S asbs bsas ε (e) S a S+S SS S* (S) 4. Construa uma gramática livre de contexto para os números romanos (1 a 10). 5. Construa uma G.L.C. para as expressões aritméticas de inteiros e identificadores com as quatro operações básicas (+, -,, /). 9

2.3 Características da linguagem P ASCAL jr 1. Não é caso sensitivo ( A = a ) 2. Suporta os tipos: integer, real, char, string e boolean 3. Comandos: Atribuição com operadores: :=, +=, -=, *=, /=, ++, - - Entrada com o comando read( ) Saída com os comandos write( ) e writeln() Condicional com o comando if - then - else Repetições: Pré teste com o comando while - do Pós teste com o comando repeat - until Contada com o comando for - to - do Sub-rotinas através dos comando procedure e function. Retorno de funções com o comando result Nome de identificador de subrotinas inicia obrigatoriamente com (ex.: Tela) 4. Constantes caracteres delimitados por ( ) e constantes strings por ( ) 5. Operadores relacionais: =, >=, <=, >, <, <> 6. Operadores lógicos and, or, xor (ou exclusivo), not 7. Operadores aritméticos: +, -, *, /, ** (potenciação) 8. Suporta operadores ternários: Expr? valor1 : valor2 9. Precedência de operadores: (a) =, +=, -=, *=, /=, ++, - - (b) and, or, xor (c) =, >=, <=, >, <, <> (d) not (e) +, - (f) *, / (g) ** (h) ( ) (i) - 10. Símbolos especiais:,, :, ;, (, ),. 10

11. Bloco de comandos delimitados por begin e end 12. Comentário de linha com operador // 13. Comentário de bloco com os delimitadores { e } 14. Lista de Palavras Reservadas: var, const, while, do, for, read, write, writeln, if, then, else, true, false, integer, real, char, string, boolean, result, procedure, function,and,or,xor,not,to,repeat,until,program,downto Exemplos de programas a serem reconhecidos pela linguagem P ASCAL jr : { } PILOTO.TXT Exemplo completo de programa na linguagem PASCALjr Desenvolvido por Rogerio Eduardo da Silva Agosto, 2005 Program Piloto; // declaraç~oes de variaveis e constantes globais var: integer cont; real Nota1, Nota2, Media_das_medias, med; const: integer total = 10; // Subrotina de preenchimento de tela procedure _Tela() begin writeln("******** ENTRADA DE DADOS ***************"); writeln("digite os valores da entrada:"); end; // Calculo da media aritmetica entre duas notas func real _Media(real a, b) var: real media; begin media := (a+b)/2.0; result := media; end; // Inicio do Programa Principal begin Media_das_medias := 0; for cont=0 to total do begin _Tela(); read(nota1, Nota2); med := _Media(Nota1, Nota2); Media_das_medias += med; 11

write("media = ",med); end; write("media Geral = ",Media_das_medias/total); end. 2.3.1 Exercícios Propostos Usando a linguagem P ASCAL jr faça: 1. Um programa para cálculo do fatorial de N. 2. Um programa para cálculo de N-ésimo termo da série de Fibonacci 12

Capítulo 3 Análise Léxica 3.1 O Papel do Analisador Léxico A análise léxica é a primeira fase de um compilador e tem por objetivo fazer a leitura do programa fonte, caracter a caracter, e traduzi-lo para uma seqüência de símbolos léxicos denominados tokens, os quais são utilizados pelo analisador sintático. Exemplos de tokens são os identificadores, palavras reservadas, operadores da linguagem, etc. A interação entre análise léxica e sintática é normalmente implementada fazendo-se com que o analisador léxico seja uma sub-rotina ou co-rotina do parser (ver figura 3.1). Ao receber do parser um comando do tipo obter próximo token, o analisador léxico lê os caracteres de entrada até que possa identificar o próximo token. Programa Fonte Analisador Léxico Token Obter Próximo Token Analisador Sintático Tabela de Símbolos Figura 3.1: O papel do analisador léxico Um analisador léxico clássico pode ser entendido como um sistema de estados finitos e, portanto, utiliza-se um autômato finito para sua implementação. As principais características desse autômato: O alfabeto de entrada são os caracteres pertencentes ao arquivo fonte Cada estado final reconhece uma classe específica de tokens da linguagem fonte É denominado erro léxico a qualquer evento (durante o processo de análise léxica) que impossibilite a interpretação de um token. Uma lista de tokens é o resultado do processo de análise léxica, caso nenhum erro léxico tenha sido encontrado. 13

Porque efetuar análise léxica? Simplificação de Projeto é mais simples implementar dois analisadores distintos (para tarefas distintas) do que um analisador sintático que faça todo trabalho de forma unificada; Melhor Eficiência a análise léxica é potencialmente mais lenta que a sintática (pois efetua leitura de caracteres em disco). Técnicas de buferização de leitura podem acelerar significativamente este processo; Portabilidade as peculiaridades do alfabeto de entrada de cada linguagem podem ser tratadas exclusivamente pelo scanner. Tokens, Padrões e Lexemas Um token é um símbolo terminal da gramática da linguagem fonte sob análise. Em geral, existem diversas cadeias de caracteres para as quais o mesmo token é gerado. Essas cadeias respeitam um determinado padrão ou regra associada a esse token. Um lexema é um conjunto de caracteres que é reconhecido pelo padrão de um determinado token. Exemplo: const pi = 3.14159; a subcadeia pi é um lexema para o token identificador, pois respeita o padrão para os identificadores (letra)(letra digito). Atributos para os tokens Um token é comumente representado como um par [LEXEMA, CLASSE], onde a classe indica qual foi o padrão utilizado para reconhecer o lexema. Outras informações adicionais podem ser incorporadas à descrição do token, de acordo com as necessidades das fases subseqüentes, como por exemplo, número da linha e coluna onde o token foi reconhecido no arquivo fonte e número de caracteres lidos até o reconhecimento, seria exemplos de informações adicionais úteis caso um erro léxico seja detectado. 3.2 Buferização de Entrada Conforme já visto, o processo de análise léxica é normalmente realizado efetuando-se uma leitura do arquivo fonte de entrada, caracter a caracter, o que resulta em um processo significativamente lento. Existem 3 alternativas de implementação de analisadores léxicos (listados em ordem crescente de complexidade de implementação): 1. Usar ferramentas de construção de analisadores léxicos (como o Lex), através de expressões regulares; 14

2. Escrever um programa numa linguagem de programação convencional, usando seus recursos de entrada e saída; 3. Escrever um programa numa linguagem de montagem e manipular explicitamente a entrada e a saída. Alguns aspectos a serem considerados no projeto de implementação de um scanner: Buffer Em muitas linguagens, existem momentos que o analisador léxico precisa examinar vários caracteres à frente do lexema, antes que seja anunciado um reconhecimento. Os caracteres que foram lidos e não foram aproveitados no lexema sob análise, são então, devolvidos ao fluxo de entrada para que possam ser lidos novamente na análise de outro lexema posterior. Assim sendo, um buffer de entrada que acumula vários caracteres é criado, conforme a figura 3.2. O processo de análise léxica é realizado sobre este buffer. Os tokens que foram reconhecidos são eliminados do buffer e novos caracteres são adicionados a ele até que todo o arquivo fonte seja lido e analisado. E = m * c * c eof apontador Figura 3.2: Buffer de entrada para um analisador léxico Em casos mais simples, a entrada pode ser realizada caracter a caracter, contendo apenas um buffer de armazenamento dos caracteres lidos. 3.3 Gramáticas e Linguagens Regulares A seguir, serão revisados alguns conceitos importantes da disciplina linguagens formais e máquinas (LFM) para então prosseguir na análise léxica. Gramática Uma gramática é um mecanismo gerador de sentenças de uma dada linguagem. É definida pela quádrupla (V N, V T, P, S), onde: V N representa o conjunto de símbolos não-terminais da linguagem; V T representa o conjunto de símbolos terminais ou alfabeto; P é um conjunto de regras de produção e S é o axioma da gramática (símbolo inicial). As regras de produção são definidas na forma α β 1 β 2... β N, onde α representa um símbolo não-terminal e os β N representam sentenças podendo conter tanto símbolos terminais quanto não-terminais. 15

Seqüência de Derivação Entende-se por derivação ao processo de substituição de α por um dos β N na regra de produção, desta forma obtendo-se uma nova sentença que por sua vez, pode ser novamente derivada por outra regra. Uma seqüência de derivação é uma série de derivações sucessivas que permitem a geração de uma determinada sentença da linguagem. Gramática Regular Uma gramática é dita ser regular se todas as suas regras de produção respeitam a forma A αb ou A α, onde A,B são símbolos não-terminais e α é uma sentença contendo somente símbolos terminais. Gramática Linearmente à Esquerda e à Direita Quando uma regra de produção é da forma A αb, ou seja, novos símbolos não-terminais são inseridos à direita da sentença, diz-se se tratar de uma gramática linearmente à direita. Se a produção for da forma A Bα denomina-se como linearmente à esquerda. Expressões Regulares Uma expressão regular representa uma determinada linguagem através de fórmulas indutivas. Simbologia adotada: ε = sentença vazia (comprimento = 0); a b = representa uma seleção entre a sentença a ou b; A = conjunto de todas as sentenças de comprimento 0 sobre A; A + = A {ε} = fechamento positivos sobre A A? = representa que a expressão A ocorre zero ou uma vez. Exemplos: Digito(Digito) = representa a descrição de números inteiros. Letra(Letra Digito) = representa a descrição de identificadores. 3.3.1 Exercícios Propostos 1. Defina expressões regulares e sua respectiva gramática regular para as seguintes linguagens: todas as palavras contendo a e/ou b. todas as palavras contendo a e/ou b com sufixo aa. todas as palavras contendo a e/ou b com aaa como sub-palavra. todas as palavras contendo a e/ou b com exatamente dois b. 2. Para a gramática G=({S,A,B},{0,1},P,S), indique a linguagem reconhecida. 16

P: S 0S A A A1 B B 0 1 ε 3.4 Especificação e Reconhecimento de Tokens A especificação de tokens é feita através de expressões regulares e reconhecida através dos reconhecedores de gramáticas regulares chamados de autômatos finitos. Exemplo: < Numero > < Digitos >< Frac Opc >< Exp Opc > < Frac Opc >. < Digitos > ε < Exp Opc > (E e)(+ ε) < Digitos > ε < Digitos > < Digito >< Digitos > < Digito > < Digito > 0 1 2... 9 esta gramática é capaz de reconhecer números inteiros como 1, 100, 1234, etc. e também números reais expressos ou não por notação exponencial como: 1.5, 10.34, 1.3e15, 1E+2; porém, é incapaz de reconhecer números como 1., sem a parte fracionária. A figura 3.3 reconhece esta gramática, enquanto que a figura 3.4 reconhece identificadores simples 1. OUTRO INICIO DÍGITO DÍGITO DÍGITO 0 1 DIGITO. 2 DIGITO 3 E e + - 4 5 DIGITO 6 OUTRO 7 * Retornar(Num_Real, Obter_Token()) OUTRO E e DIGITO 8 * Retornar(Num_Inteiro, Obter_Token()) Figura 3.3: Autômato finito de reconhecimento de números inteiros e reais O reconhecimento de strings é apresentado na figura 3.5, onde caracteres válidos representa o alfabeto válido para strings, geralmente letras, números, espaços e sinais ortográficos. Exercício: Criar um AFD capaz de reconhecer os tokens da linguagem P ASCAL jr : símbolos (Dois Pontos, Ponto e Vírgula, Vírgula, Abre e Fecha Parênteses, Atribuição), Operadores Relacionais e Aritméticos, Constante Caracter e identificadores de sub-rotinas 1 Exceto identificadores de sub-rotinas 17

INICIO LETRA OU DÍGITO LETRA OUTRO 0 1 2 * Retornar(ID, Obter_Token()) Figura 3.4: AFD de reconhecimento de identificadores simples INICIO CARACTERES VÁLIDOS " 0 1 " 2 Retornar(String,Obter_Token()) Figura 3.5: AFD de reconhecimento de strings (iniciam obrigatoriamente com e tem pelo menos 2 caracteres), e ainda, ser capaz de tratar os caracteres nulos: espaços, enter, tab e comentários, sem reconhecer token. Reconhecendo palavras reservadas como identificadores simples: Criar uma função de identificação de palavras reservadas (enumeração) que retorna a classe palavra reservada ou identificador. USO: Retornar(ObterClasse(Lexema),Lexema) Erros Léxicos 1. Caracter Inválido : uso de um caracter (simbolo) de entrada (arquivo fonte) que não pertença ao alfabeto da linguagem. Exemplo: # ou % 2. Delimitador Não Balanceado : definição de uma cadeia literal (ou constante caracter) sem o correto balanceamento das aspas. Exemplo: Entrada de Dados 3. Número Real Inválido : definição incorreta ou incompleta de um número real. Exemplos: 1., 1.0e3,.8, 1e+ O código abaixo apresenta algum erro léxico? Apresente a lista léxica. begin ; <>media==10.5e-5 /===//%_ Média 1.P Teste? Solução: begin Palavra Reservada ; Símbolo Ponto e Virgula 18

<> Operador Relacional Diferente media Identificador = Operador Relacional de Igualdade = Operador Relacional de Igualdade 10.5E-5 Número Real /= Símbolo de Atribuição = Operador Relacional de Igualdade = Operador Relacional de Igualdade Teste Identificador? Símbolo Interrogação 3.4.1 Trabalho Prático #1 Implementar um módulo (sub-rotina) analisador léxico para um protótipo de compilador para a linguagem P ASCAL jr vista em aula. Características: Do módulo scanner: A sub-rotina retorna um token (classe e lexema) cada vez que for chamada. Considera que o programa fonte para análise já está aberto. Não retorna nada quando atingir o fim de arquivo (flag de controle). Implementa um AFD para o reconhecimento de tokens. Do programa a ser criado: Abre um arquivo fonte para análise. Chama (sucessivas vezes) a rotina de scanner e exibe o valor do token. Fecha o arquivo fonte ao final da compilação. Pára o processo de compilação caso um erro seja encontrado. Exibe erros de compilação (se ocorrerem) ou mensagem de sucesso. Critérios de Avaliação: Implementação usando linguagem C ou C++. Entrega de fontes e executável (em um arquivo zipado) via disquete/cd ou e-mail: rsilva@joinville.udesc.br ou professor.rogerio@gmail.com 19

Grupo de 02 alunos (máximo). Valor do trabalho: 10.0 (25% da nota prática). Data de Entrega: A Definir Punições: de 10% por cada análise incorreta. de 20% do valor do trabalho por dia de atraso. de 20% do valor do trabalho para a entrega não conforme dos arquivos pedidos. de 50% do valor do trabalho para o caso de não executar ou travar (após teste em 2 computadores, sendo um o do professor). de 100% do valor do trabalho para o caso de cópias (mesmo de trabalhos de semestres anteriores). Prazo máximo para defesa e arguição sobre o trabalho: 5 dias letivos após entrega. Punições: de 25% para arguição não respondida ou respondida incorretamente. Obs.: A arguição é individual. de 33% ponto por dia de atraso da defesa. 20

Capítulo 4 Análise Sintática 4.1 O Papel do Analisador Sintático A análise sintática constitui a segunda etapa de um tradutor. Sua função é verificar se as construções usadas no programa estão gramaticalmente corretas. Normalmente, as estruturas sintáticas válidas são especificadas através de uma gramática livre de contexto. Dada uma GLC e uma sentença (programa fonte) s, o objetivo do parser é verificar se s pertence a GLC, através da construção de uma árvore de derivação. O processo de construção dessa árvore pode ser feito de forma explícita (construindose o TDA) ou implícita, através de chamadas recursivas das rotinas que aplicam as regras de produção da gramática durante o reconhecimento. Existem duas estratégias básicas: Descendente (Top-Down) e Ascendente (Bottom- Up). Na estratégia top-down constrói-se a árvore a partir da raiz em direção às folhas (tokens), enquanto que na bottom-up, o processo é invertido e a construção é realizada partindo-se das folhas, agrupando-se os tokens até que a raiz da árvore seja gerada. A árvore gramatical é então a saída para as próximas fases da compilação. Revisão sobre Gramáticas Livre de Contexto (GLC) Uma gramática livre de contexto é qualquer gramática da forma: A α, onde A é um símbolo não-terminal e α um elemento pertencente a (V N V T ). Exemplo de produções de uma G.L.C.: S SS+ SS a. Árvores de Derivação Árvore de derivação é a representação gráfica de uma derivação de sentença. Exemplo: Considerando a gramática abaixo, gerar árvore de derivação que comprova que a sentença 45 é válida (ver Figura 4.1). 21

< Numero > < Num > < Num > < Num >< Digito > < Digito > < Digito > 0 1 2... 9 <Numero> <Num> <Num> <Digito> <Digito> 5 4 Figura 4.1: Exemplo de Árvore Sintática Derivação mais à Esquerda e mais à Direita Derivação mais à esquerda é obtida por gramáticas que geram inicialmente, os símbolos mais à esquerda da sentença sob análise; analogamente para as derivação mais à direita. Exemplo: Seja a gramática: E E + E E E E E E/E (E) x. Pode obter a expressão x+x*x de duas formas, conforme a figura 4.2: E E E * E E + E E + E X X E * E X X X X Figura 4.2: Derivação à Esquerda e à Direita Exercício: Para a gramática G = ({S,A},{0,1},P,S) sendo P: S 0S A A 1A 1, provar as seguintes sentenças: 0001, 01, 0011. 22

4.2 Análise Sintática Ascendente - BOTTOM UP A criação da árvore gramatical é realizada no sentido folhas raiz, ou seja, geração de sentenças é feita através do processo de empilhar e reduzir. A idéia é reduzir a sentença original até o axioma da gramática através de sucessivas substituições por não-terminais. Exemplo: S aabe A Abc b B d Verificar se a sentença abbcde pode ser reduzida pela gramática: abbcde aabcde aade aabe S 4.2.1 Algoritmo Empilhar-e-Reduzir Este procedimento de análise sintática ascendente consiste de dois passos: 1. Escolha de um candidato α a redução (handle); 2. Redução do candidato pelo não-terminal A à esquerda da produção A α; 3. Repetir os passos 1 e 2 até que a sentença tenha sido reduzida ao axioma da gramática. Um candidato é uma subcadeia que reconhece o lado direito de uma produção e cuja redução ao não-terminal do lado esquerdo da produção representa um passo ao longo do percurso de uma derivação. É denominado de poda do candidato ao processo de substituí-lo pelo não-terminal à esquerda da regra de produção, obtendo desta forma, uma redução na sentença sob análise. Uma forma conveniente de implementar um analisador sintático de empilhar e reduzir é usar uma pilha para guardar os símbolos gramaticais. O analisador sintático opera empilhando zero ou mais símbolos até que um candidato surja no topo da pilha. Uma poda do candidato é então feita. Repete-se este processo até que no topo da pilha esteja o axioma da gramática ou um erro seja encontrado (nenhuma poda seja possível). Exemplo: E E + E E E E E E/E (E) id. Sentença sob análise: id + id * id 23

Entrada Pilha Ação id+id*id $ empilhar +id*id $id reduzir E id +id*id $E empilhar id*id $E+ empilhar *id $E+id reduzir E id *id $E+E reduzir E E + E *id $E empilhar id $E* empilhar $ $E*id reduzir E id $ $E*E reduzir E E E $ $E aceitar São apenas 4 as operações possíveis por este método: empilhar, reduzir, aceitar ou erro. Conflitos durante a Análise Sintática de Empilhar e Reduzir Existem gramáticas livres de contexto para as quais o procedimento empilhar-e-reduzir não pode ser utilizado, porque, em certos casos, o analisador pode atingir um estado tal, que: Mesmo conhecendo toda a pilha e o próximo símbolo de entrada, não pode decidir entre empilhar e reduzir. Isto é chamado de conflito empilhar/reduzir. Outro conflito possível, o reduzir/reduzir, ocorre quando não é possível optar entre as diversas reduções possíveis. 4.3 Análise Sintática Descendente - TOP DOWN A análise sintática top-down pode ser vista como uma tentativa de se encontrar uma derivação mais à esquerda para uma cadeia de entrada, ou ainda, como de se construir a árvore gramatical a partir da raiz em direção às folhas. O processo de análise pode ser feito de forma recursiva ou não, onde a forma recursiva pode ser realizada com ou sem retrocesso (backtracking), dependendo das regras de produção gramática. Análise Sintática Recursiva com Retrocesso A construção da árvore é feita a partir da raiz, expandindo sempre o não-terminal mais à esquerda primeiro. Quando existe mais de uma regra de produção para o não-terminal a ser expandido, a opção escolhida é função do símbolo corrente na fita de entrada (token sob análise). Se o token não define a produção a ser usada, então todas as alternativas vão ser tentadas até que se obtenha sucesso (ou todas falhem). Exemplo 1:S cad A ab a. Verificar se a gramática gera a sentença cad. Exemplo 2: S ca A ab B D bd D d 24

S c A d falha! S S a b c A d S c A d sucesso! a Figura 4.3: Análise descendente com backtracking A análise sintática é dita ser uma análise sintática preditiva caso não seja necessário a realização de retrocesso no processo e pode ser implementada de forma recursiva ou não (através da utilização de uma pilha). 4.3.1 Análise Sintática Preditiva O processo de análise preditiva (sem retrocesso) exige modificações na gramática original para análise: eliminação de recursão à esquerda; fatoração à esquerda das regras de produção; os não-terminais que apresentarem mais de uma regra de produção, tenham o primeiro terminal derivável único (capaz de identificar a produção a ser analisada). Ou seja, deve ser possível determinar, para um dado símbolo a, qual das produções deve ser derivada. Exemplo: No exemplo 2 visto acima a produção B D bd D d apresenta duas alternativas de derivação. A escolha é feita a partir do primeiro terminal para cada regra (d ou b). O conjunto de símbolos terminais que iniciam sentenças deriváveis a partir de uma produção b é denominado FIRST(β) ou PRIMEIRO(β). Exemplo: FIRST(S) = {c}; FIRST(A) = {a}; FIRST(B) = {b, d}; FIRST(D) = {d}. As regras que definem o conjunto FIRST são: 25

Se β ε, então ε é um elemento de FIRST. Se β aδ, sendo a um símbolo terminal, então a pertence a FIRST. Se β X 1 X 2... X N, sendo X 1 X 2... X N elementos não-terminais, então FIRST(β) = FIRST(X 1 ). Se em FIRST(X 1 ) constar o elemento ε, então incluir FIRST(X 2 ) em FIRST(β) e assim por diante. Eliminação da Recursão à Esquerda É possível que um analisador gramatical descendente recursivo execute indefinidamente. O problema ocorre em produções recursivas à esquerda, tais como: A A0 1. Este tipo de produção gera uma árvore que cresce recursivamente à esquerda até que um terminal 1 seja gerado à esquerda da seqüência de 0 s. Para se evitar isso deve-se substituir o elemento causador da recursão à esquerda, que é do tipo A Aα β, onde α, β representam outras seqüências de terminais e não-terminais não iniciadas por A. Para eliminar a recursão à esquerda deve-se reescrever essa produção, da seguinte forma: A βa e A αa ε. A figura 4.4 apresenta as árvors de derivação para uma sentença qualquer da forma βαααα. 4.3.2 Exercícios Propostos Para as gramáticas abaixo elimine sua recursão à esquerda. 1. G=({S,A,B},{a,b},P,S) onde P: S Sa Sb A B A Aa a B bb b 2. G=({S,A},{0,1,2},P,S) onde P: S S0 S1 A 0 A S2 3. G=({S,A,B},{0,1},P,S) onde P: S SA A A A0B 0 B B1 ε 4. G=({A},{0,1},P,A) onde P: A A0A 1 Apresente a cláusula First para as produções das gramáticas abaixo: 1. G=({S,X,Y,Z},{0,1,2,3},P,S) onde P: S XY Z X OXO 1 Y 2Y 2 3 Z 0Z1 ε 2. G=({S,A,B,C},{a,b,c},P,S) onde P: S Sa aa A aa Bb B cb ε 3. G=({S,X,Y,Z},{0,1},P,S) onde P: S XY Z X 0X 1Y ε Y 1Y ε Z 01Z ε Fatoração à Esquerda A fatoração à esquerda é uma transformação gramatical útil para a criação de uma gramática adequada à análise sintática preditiva. A idéia básica está em, quando não estiver claro qual das duas produções alternativas usar para expandir um não-terminal A, estarmos capacitados a reescrever as produções A e postergar a decisão até que tenhamos visto o suficiente da entrada para realizarmos a escolha certa. Exemplo: S axby cz axby 26

A A A β A α A α A α α β α A' A' α A' α A' α A' ε Figura 4.4: Exemplos de Recursão à Esquerda e à Direita Ao analisarmos o token a não há como saber qual das duas alternativas utilizar (o comando com ou sem o cz ). Quando houver duas produções A αβ 1 αβ 2, devemos postergar a decisão expandindo A para αa e então expandir A para β 1 β 2. Fatorando esta gramática temos: S axby S S cz ε. 4.4 Reconhecedor de Gramáticas Preditivas Descendentes Um reconhecedor preditivo descendente (orientado por tabela) compreende uma fita de entrada, uma pilha e uma tabela de análise, conforme é mostrado na figura 4.5. A fita contém a sentença a ser analisada seguida de $. A pilha contém os símbolos utilizados durante o processo de análise. A tabela de análise é uma matriz com n linhas (correspondendo aos símbolos não-terminais) e t+1 colunas (correspondendo aos símbolos terminais mais o símbolo especial $). Considerando X o elemento no topo da pilha e a o símbolo de entrada sob análise, o analisador executa uma de três ações possíveis: 1. se X = a = $, o analisador pára, aceitando a sentença; 2. se X = a $, o analisador desempilha a e avança o cabeçote de leitura para o próximo símbolo na fita de entrada; 3. se X é um símbolo não-terminal, o analisador consulta a tabela M[X,a] da tabela de análise. Essa entrada poderá conter uma produção da gramática ou ser vazia. Supondo M[X,a] = { X XY Z }, o analisador substitui X (no topo da pilha) por ZYX (ficando X no topo). Se M[X,a] for vazio isto é um erro sintático. Na implementação de um analisador sintático, a maior dificuldade está na construção da tabela de análise. Para construir essa tabela, é necessário computar duas funções associadas à gramática: FIRST e FOLLOW. 27

a + b $ X Y Z Parser Tabela de Análise Figura 4.5: Funcionamento de um Analisador Sintático Descendente O algoritmo para calcular a função FIRST já foi visto anteriormente. O algoritmo para calcular a função FOLLOW é apresentado a seguir: 1. Se S é o símbolo inicial da gramática e $ é o marcador de fim de sentença, então $ está em FOLLOW(S); 2. Se existe produção do tipo A αxβ, então todos os terminais de FIRST(β), fazem parte de FOLLOW(X); 3. Se existe produção do tipo A αx, ou A αxβ, sendo que β ε, então todos os terminais que estiverem em FOLLOW(A) fazem parte de FOLLOW(X). Dada a gramática G = ({E, E, T, T, F }, {,,, id}, P, E) para expressões lógicas: Cláusula First E T E E T E ε T F T T F T ε F F id Convém iniciar o processo pelos não-terminais que gerem conjuntos triviais. No exemplo, temos os não-terminais F, E e T que só geram elementos terminais (ou vazio): F = {, id} E = {, ε} T = {, ε} Como T deriva apenas em FT e F não leva em vazio, conclui-se que FIRST(T) = FIRST(F). E ainda, FIRST(E) = FIRST(T) = FIRST(F) = {, id}. 28

Cláusula Follow Pela regra 1 temos que FOLLOW(E) = {$}. Pela regra 3 tem-se que FOLLOW(E) = FOLLOW(E ). FOLLOW(T) é obtido a partir da união dos conjuntos obtidos pela aplicação da regra 2 em (E T E ) e regra 3 em (E ε). Sendo assim temos: FOLLOW(T) = FIRST(E ) + FOLLOW(E ) = {, $}. FOLLOW(T ) = FOLLOW(T) pela aplicação da regra 3 em T F T. E finalmente, FOLLOW(F) = FIRST(T ) + FOLLOW(T ). Aplicação das regras 2 e 3 em T F T ε, ou seja FOLLOW(F) = {,, $}. 4.4.1 Algoritmo para Construção da Tabela de Análise Método: Para cada produção X α, execute os passos 2 e 3 (para criar a linha X da tabela M); Para cada terminal a de FIRST(α), adicione a produção X α a M[X,a]; Se FIRST(α) inclui a palavra vazia, então adicione X α a M[X,b] para cada b em FOLLOW(X); Aplicando-se o algoritmo acima à gramática de expressões lógicas temos: Para E T E tem-se FIRST(TE ) = {, id} então, M[E, ] = M[E,id] = E T E. Para E T E tem-se FIRST( T E ) = { } então, M[E, ] = E T E. Para E ε tem-se FOLLOW(E ) = {$} então, M[E, $] = E ε. Para T F T tem-se FIRST(FT ) = {, id} então, M[T, ] = M[T,id] = T F T. Para T F T tem-se FIRST( F T ) = { } então, M[T, ] = T F T. Para T ε tem-se FOLLOW(T ) = {, $} então, M[T, ] = M[T,$] = T ε. Para F F tem-se FIRST( F ) = { } então, M[F, ] = F F. Para F id tem-se FIRST(id) = {id} então,m[f,id] = F id. id $ E E T E E T E E E T E E ε T T F T T F T T T ε T F T T ε F F id F id Se, em cada entrada da Tabela de Análise, existe apenas uma produção, então a gramática que originou a tabela é dita ser do tipo LL(1), ou seja: as sentenças geradas pela gramática são passíveis de serem analisadas da esquerda para a direita (Left to Right), produzindo uma derivação mais à esquerda (Leftmost Derivation), levando em conta apenas um símbolo da entrada. Exercício: Considerando a gramática para a linguagem a ser reconhecida pelo protótipo de compilador para análise descendente, construir a tabela de análise resultante. 29

4.4.2 Projeto de uma Gramática para um Analisador Sintático Preditivo Ascendente Analisa gramáticas do tipo LR(k), ou seja, left-to-right e rightmost derivation com k símbolos lidos da entrada a cada etapa de análise. Porque usar análise sintática ascendente LR? porque é possível ser elaborados reconhecedores para todas as GLC, sem restrição; porque o método de análise LR é tão eficiente quanto os demais métodos de análise; porque um analisador LR consegue encontrar um erro sintático o mais cedo possível em uma análise da esquerda para a direita. As gramáticas GLC para as quais é viável a implementação manual de reconhecedores ascendentes (devido à complexidade de implementação) apresentam as seguintes restrições: nenhum lado direito das produções seja ε nenhum lado direito tenha dois não-terminais adjacentes (gramática de operadores) Exemplo: E E + E E E E E E/E (E) E id Um forma simples de se implementar um reconhecedor ascendente é através da análise de precedência de operadores, porém, justamente devido à sua simplicidade, uma série de restrições estão associadas a estes: dificuldades de analisar operadores com mais de um significado semântico (ex.: operador unário e binário de subtração) somente uma pequena classe de linguagens pode ser analisada por esta alternativa, apesar disso, já foram desenvolvidos analisadores de precedência para linguagens inteiras. Na análise de precedência temos definidos as relações de precedência entre os operadores, sendo (a < b) onde a confere precedência a b; (a = b) onde a possui a mesma precedência de b e (a > b) a tem precedência sobre b. Seja o exemplo da gramática anterior onde a precedência dos operadores é dada por: id + * $ id > > > + < > < > * < > > > $ < < < Analisando a expressão: id+id*id temos as seguintes relações de precedência: $ < id > + < id > < id > $ O algoritmo para se determinar o handle para redução é: 30